Dependency Injection, iOS and You

Graham Lee's Headshot
Graham Lee

Object-oriented programming is all about describing your software problem in terms of a network of objects and the messages they send to each other. In an iOS app, this network starts with the UIApplication instance, which sets up a load of framework objects before telling your app delegate that it has finished launching.

The app delegate is the seed from which you build the object graph that supplies your app's features: the models, views and controllers all have to start somewhere, and this is the place. But it's important that this graph not be brittle. If the objects in your app make too many expectations on what's going on around them, it can be hard to adapt them to new requirements, make bug fixes, add new features, and test components in isolation.

What's a dependency injection and why should I buy one?

The reason brittle object graphs are bad is that you cannot easily replace parts of the application. If an object expects to ask its environment for a load of other objects around it, then you cannot simply tell it that it should be using another object. Dependency injection fixes that. It tells the object, "Hey, these are the objects you should work with," so that if we want to change the collaborators we just inject different things.

Why is that useful? In a previous post, I described how mock objects can be used as stand-ins for production objects, to aid in designing their interaction and to remove complicated behaviour in the tests. That's one reason to want to swap collaborators.

Another benefit is that algorithms can be reused in multiple contexts, receiving data from different sources and delivering results to different destinations. To illustrate this situation, consider the difference between an iPod and a music box.

Music box "Music Box in the grass-1" by Flickr user zeevveez

The tune that a music box can play is encoded in the pattern of notches on the drum. If you want to play a different tune, you have to take the whole device apart, replace the drum, and put it back together. Conversely, an iPod has a USB interface for giving it different tunes. It's just a player of noise, and the specific noise is easy to change.

Similarly, the music box only has one output mechanism: the metal prongs that are picked by the prongs on the drum (musical instrument buffs will recognise that this makes the music box a kind of lamellophone). As with changing the tune, changing how it plays that tune involves disassembling the box and replacing the prongs. Meanwhile, the iPod just has an output socket. Anything that has a plug compatible with that socket and the signals produced can play the music being generated by the iPod. If you're bored of headphones and want to play your Herb Alpert tunes over the public address system at the Rose Bowl, you simply unplug one and plug in another, almost literally injecting that dependency into the iPod.

So, back to programming. Dependency injection is all about increasing the flexibility and adaptability of code modules such as classes. What does that look like in Objective-C?

Worked example

Imagine the following class is part of an existing app to browse open-source code. Part of its feature set lets us see the home pages of a couple of code hosting websites.

@interface BNRCodeHostFetcher : NSObject
    
    - (void)fetchGithubHome;
    - (void)fetchBitbucketHome;
    
    @end
    
    @implementation BNRCodeHostFetcher
    
    - (void)fetchGithubHome
    {
        NSURLSession *session = [NSURLSession sharedSession];
        NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.github.com"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
            NSDictionary *userInfo = @{ @"data": data,
                                        @"response": response,
                                        @"error": error };
            [[NSNotificationCenter defaultCenter] postNotificationName:@"BNRGithubFetchCompletedNotification" object:self userInfo:userInfo];
        }];
        [task resume];
    }
    
    - (void)fetchBitbucketHome
    {
        NSURLSession *session = [NSURLSession sharedSession];
        NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.bitbucket.org"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
            NSDictionary *userInfo = @{ @"data": data,
                                        @"response": response,
                                        @"error": error };
            [[NSNotificationCenter defaultCenter] postNotificationName:@"BNRBitbucketFetchCompletedNotification" object:self userInfo:userInfo];
        }];
        [task resume];
    }
    
    @end

As is often the way with legacy code, there's a lot to dislike about the design of this class. There's plenty of duplication and notifications are an "interesting" choice for communicating completion to the outside world, but most importantly for our purposes, it relies on grabbing shared objects from its environment: the URL session and the notification centre. If we want to test this class in isolation, we'll need to be able to substitute mocks for these objects, and you can imagine wanting to change the URL session to get different session configurations, perhaps to change the caching behaviour.

These are, to use the nomenclature from Michael Feathers' Working Effectively with Legacy Code, hidden dependencies. If you're looking at the class interface, you cannot see anything to suggest that the class might depend on an NSURLSession or NSNotificationCenter, but there they are.

A bit of refactoring

The first change is to make these dependencies explicit (and slightly reduce the code duplication along the way) by extracting a getter for each of the shared objects. This doesn't change the behaviour, but as the class isn't yet under test, it's best to take baby steps.

@interface BNRCodeHostFetcher : NSObject
    
    @property (nonatomic, readonly) NSURLSession *session;
    @property (nonatomic, readonly) NSNotificationCenter *notificationCenter;
    - (void)fetchGithubHome;
    - (void)fetchBitbucketHome;
    
    @end
    
    @implementation BNRCodeHostFetcher
    
    - (NSURLSession *)session
    {
        return [NSURLSession sharedSession];
    }
    
    - (NSNotificationCenter *)notificationCenter
    {
        return [NSNotificationCenter defaultCenter];
    }
    
    - (void)fetchGithubHome
    {
        NSURLSession *session = [self session];
        NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.github.com"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
            NSDictionary *userInfo = @{ @"data": data,
                    @"response": response,
                    @"error": error };
            [[self notificationCenter] postNotificationName:@"BNRGithubFetchCompletedNotification" object:self userInfo:userInfo];
        }];
        [task resume];
    }
    
    - (void)fetchBitbucketHome
    {
        NSURLSession *session = [self session];
        NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.bitbucket.org"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
            NSDictionary *userInfo = @{ @"data": data,
                    @"response": response,
                    @"error": error };
            [[self notificationCenter] postNotificationName:@"BNRBitbucketFetchCompletedNotification" object:self userInfo:userInfo];
        }];
        [task resume];
    }
    
    @end

Now there's a single place to go to affect the collaborating objects used by each of the fetch methods: If the accessor returns a different object, that will be used. To continue using Michael Feathers' terminology, these accessors have introduced a seam where two parts of the app's behaviour come together. In this case, the seam is between the use of framework objects, and the supply of framework objects.

The next task is to introduce an inflection point, a mechanism for taking advantage of the seam. Chained initialisers are one way to let users of this class inject those framework objects. Deleting the custom accessors we just introduced means that the class will use whatever was passed in its initialiser, instead of the objects it had discovered itself. It's not the only way: Subclassing and overriding the accessors would also work. In this example, we'll explore the approach using a custom initialiser.

@interface BNRCodeHostFetcher : NSObject
    
    @property (nonatomic, readonly) NSURLSession *session;
    @property (nonatomic, readonly) NSNotificationCenter *notificationCenter;
    
    - (instancetype)initWithURLSession:(NSURLSession *)session notificationCenter:(NSNotificationCenter *)center;
    //...    
    @end
    
    @interface BNRCodeHostFetcher ()
    
    @property (nonatomic, strong, readwrite) NSURLSession *session;
    @property (nonatomic, strong, readwrite) NSNotificationCenter *notificationCenter;
    
    @end
    
    @implementation BNRCodeHostFetcher
    
    - (instancetype)initWithURLSession: (NSURLSession *)session notificationCenter: (NSNotificationCenter *)center
    {
        self = [super init];
        if (self)
        {
            self.session = session;
            self.notificationCenter = center;
        }
        return self;
    }
    
    - (instancetype)init
    {
        return [self initWithURLSession:[NSURLSession sharedSession]
                    notificationCenter:[NSNotificationCenter defaultCenter]];
    }
    
    //...
    @end

What did that get us?

There's no change to the behaviour in the app: it still sets up its code host fetcher with -init, which means it uses the default NSNotificationCenter and NSURLSession. But now this class can be tested. You could use OCMock or a similar tool to inspect the collaboration between the code host fetcher and its collaborators. Once you can test a class in isolation, validating and correcting its behaviour becomes much easier.

That's it?

Yes, there's nothing up my sleeve; that's really all there is to dependency injection. As James Shore put it:

"Dependency Injection" is a 25-dollar term for a 5-cent concept.

There's no need for drastic rework to introduce new frameworks or libraries, though those libraries do exist. All you really need to do to get started is to look at an object, work out what else it needs to talk to, and then decide how you can get it talking to something else instead.

Recent Comments

comments powered by Disqus