Breaking Dependencies on Configs

| Comments

Cleaning out dependencies is pretty much always a good thing. It means more loosely-coupled code, easier testing, and safer execution.

I often catch myself not going far enough in removing dependencies and making my code injectable. Configurations are a good target for some focused DI.

Take a data adapter for user articles. Imagine a blogging platform where users have any number of articles. For the sake of the example, assume that the ‘data’ attribute is an object that is good at getting persistent data from a database or something similar. It’s not crucial.

class ArticleAdapter(object):
    ##
    # Get all the articles authored by a user
    # @param username The username string for a user.
    # @return A List of Article Instances for the user, None if there 
    #       are no matching Articles.

    def get_article_by_user(self, username):
        return self.data.whereUserName(username) 

Very straightforward, and it does the job nicely.

Now our wildly successful blogging system has some users with thousands of articles. We don’t want to get them all every time, so we put a limit on the number of Articles fetched. Being sweet programmers, we make the max count available via a ConfigObj:

class ArticleAdapter(object):
    def __init__(self):
        self.config = ConfigObj('/app/config/articles.cfg')

    def get_article_by_user(self, username):
        max_count = self.config['max_articles']
        return self.data.whereUserName(username, max=max_count) 

Nice. We’ve got a configurable limit to keep Outrageous Things from happening. but, we’ve created a dependency on the ConfigObj. The init method calls the ConfigObj with a hardcoded path. This is going to make testing nasty. Luckily we can shuffle things to extract the dependency, and make the ConfigObj injectable:

class ArticleAdapter(object):

    ##
    # @param config A ConfigObj instance 
    #       (or duck of a similar feather)

    def __init__(self, data):
        self.data = data
        self.config = None

    ##
    # Get all the articles authored by a user.
    # @param username The username string for a user.
    # @return A List of Article Instances for the user, None if there 
    #       are no matching Articles.

    def get_article_by_user(self, username):
        max_count = self.config['max_articles']
        return self.data.whereUserName(username, max=max_count)     

Now the constructor doesn’t need to go and create a ConfigObj. We can use setter injection to set it when the code actually runs. Writing a test for it (using Mocker for the nasty parts) is easy:

def test_gets_article_by_user():
    mocker = Mocker()
    mock_data = mocker.mock()
    mock_data.wherUserName('superdude', max=100)
    mocker.result([ Article(title="War: what is it good for"), Article(title="my favorite Jedi")])
    mocker.replay()

    fake_config = ConfigObj(['max_articles=100'])
    adapter = ArticleAdapter()
    adapter.config = fake_config
    articles = adapter.get_article_by_user('superdude')
    mocker.verify()
    assert len(articles) == 2

So we’re done. BUT… The dependency we factored out was the ConfigObj. Our class isn’t really depending on the ConfigObj. It depends on the max count value that the config provides. We can go one step further in our thinking if we consider the max row count to be an attribute of the adapter. This is one of the cool things that happens when you start figuring out dependencies: almost everything is an attribute. Moving our max from a config to an attribute, we get:

class ArticleAdapter(object):

    ##
    # @param config A ConfigObj instance (or duck of a similar feather)

    def __init__(self, data, max_articles):
        self.data = data
        self.max_articles = max_articles

    ##
    # Get all the articles authored by a user.
    # @param username The username string for a user.
    # @return A List of Article Instances for the user, None if there 
    #       are no matching Articles.       

    def get_article_by_user(self, username):
        return self.data.whereUserName(username, max=self.max_articles)     

def test_gets_article_by_user():
    mocker = Mocker()
    mock_data = mocker.mock()
    mock_data.whereUserName('superdude', max=100)
    mocker.result([ Article(title="War: what is it good for"), Article(title="my favorite Jedi")])
    mocker.replay()

    adapter = ArticleAdapter(fake_config)
    adapter.max_articles = 100
    articles = adapter.get_article_by_user('superdude')
    mocker.verify()
    assert len(articles) == 2

The big difference is that its easier to wrap your head around plain attribute. Anything fancier requires mental gymnastics, and that ain’t cool, man. So phrases like this are easy to read and understand:

adapter = ArticleAdapter()
adapter.max_articles = 100

While phrases like this require more mental gymnastics:

fake_config = ConfigObj(['max_articles=100'])
adapter = ArticleAdapter()
adapter.config = fake_config

In the first case, the mental dialog of the next developer might be, “Ok, I need to get a max record count from a ConfigObj instance, so I’ve got to create or find some one to get my answer. Better start digging…

In the second case I imagine someone thinking “Allright, max results is an attribute I can set. There may be a config somewhere I can get it from, but no biggie either way.

blog comments powered by Disqus