DEPENDENCY INJECTION FOR OBJECTIVE-C & SWIFT, WITH TYPHOON
WHAT IS DEPENDENCY INECTION?
Many people have trouble getting the hang of dependency injection, at first. And I think part of the problem is that it is actually so simple that we’re inclined to look for something more complicated. “Surely that there has to be more to it?!”, so to say. So, with that in mind, imagine that you’re writing an app that gives weather reports. You need a cloud-service (excuse the pun ) to provide the data, and at first you go for a free weather report provider, but in future you’d like to integrate a weather service with better accuracy and more features. So, as do all good object-oriented developers, you make a WeatherClient protocol and back it initially with an implementation based on the free, online data provider.
WITHOUT DEPENDENCY INJECTION, YOU MIGHT HAVE A VIEW CONTROLLER LIKE THIS:
-(id) init
{
self = [super init];
if (self)
{
//The class using some collaborating class builds its own assistant.
//it might be one of several classes using the weatherClient.
_weatherClient = [[GoogleWeatherClientImpl alloc] initWithParameters:xyz];
}
return self;
}
The thing with this approach is, if you wanted to change to another weather client implementation you’d have to go and find all the places in your code that use the old one, and move them over to the new one. Each time, making sure to pass in the correct initialization parameters. A very common approach is to have a centrally configured singleton:
_weatherClient = [GoogleWeatherClient sharedInstance];
With either of the above approaches, in order to test your view controller, you now have to test its collaborating class (the weather client) at the same time, and this can get tricky, especially as your application gets more complex. Imagine testing Class A, depends on Class B, depends on Class C, depends on… Not much fun! Sure, you could patch out the singleton with a mock or a stub, but this requires peeking inside the code to find the dependencies. Besides taking time that could be better spent else-where, this ends up becoming “glass-box” testing as opposed to “black-box” testing. Isn’t it better to be able to test the external interface to a class, without having worry about what’s going on inside? And you have to remember un-patch again at the end of the test-case or risk strange breakages to other tests, where it’s difficult to pin-point what the real problem might be. So with dependency injection, rather than having objects make their own collaborators, we have them supplied to the class instance via an initializer or property setter.
AND NOW, IT SIMPLY BECOMES:
-(id) initWithWeatherClient:(id<WeatherClient>)weatherClient
{
self = [super init];
if (self)
{
_weatherClient = weatherClient;
}
return self;
}
IS THAT ALL THEY MEAN BY ‘INJECTED’?
Yes it is. Right now, you might be thinking “Geez! That’s a pretty fancy name for something so plain.” Well, you‘d be right. But let‘s look at what happens when we start to apply this approach: Let's say you identify some hard-wired network configurations in a your GoogleWeatherClient, and correct this by instead passing them in via an initializer method. Now if you want to use this class, as a collaborator in a new class, let's say a ViewController, then your GoogleWeatherClient itself can be either a hard-wired dependency, or injected. To get the benefits of dependency injection again, we repeat the process, pulling up the class and along with its own dependencies. And we keep applying until we have a logical module or 'assembly'.
In this way dependency injection lets your application tell an architectural story. When the key actors are pulled up into an assembly that describes roles and collaborations, then the application’s configuration no longer exhibits fragmentation, duplication or tight-coupling. Having created this script that describes roles and collaborations we realize a number of benefits.
BENEFITS OF DEPENDENCY INJECTION
- We can substitute another actor to fulfill a given role. If you want to change from one implementation of a class to another, you need only change a single declaration.
- By removing tight-coupling, we need not understand all of a problem at once, its easy to evolve our app’s design as the requirements evolve.
- Classes are easier to test, because we can supply simple mocks and stubs in place of concrete collaborators. Or the real collaborators, but configured to be used in a test scenario.
- It promotes separation of concerns and a clear contract between classes. Its easy to see what each class needs in order to do its job.
- We can layout an app's architecture using stubs - the simplest possible implementation of a role - so that we can quickly see an end-to-end use-case taking place. Having done this, we can assign to other team members the responsibility of filling out these stubs for real implementations, and they can do so without breaking the app while they work, and without impacting other team members.
YOUR DEPENDENCY INJECTION OPTIONS
If you proceed with the Dependency Injection pattern (assuming you’re not one of the remaining “flat-earthers”, who believe that Objective-C somehow magically alleviates the need for common-sense: “Oh, I don’t do DI, I use swizzling class-clusters!”), then there are basically two options:
You can do dependency injection without a framework to help you. It is simple after all, and in fact I recommend you do this, at least as an exercise to fully appreciate the pattern. Still, just as you can also write tests without a test framework, or mocks without a mocking library, once an application gets more complex its good to have help if you can get it - to put the pattern on "rails", so to speak.
So, going down the framework route, there’s been quite a lot of action in Objective-C land, over the last three years. In fact, there are now around 15 Dependency Injection frameworks, many following in the footsteps of Google Guice. The authors have done a great job (Objection is especially good). However, I wanted an approach that allows the following:
DESIGN GOALS AND FEATURES
- Non-invasive. No macros or XML required.
- Uses powerful ObjC runtime instrumentation.
- No magic strings – supports IDE refactoring, code-completion and compile-time checking.
- Provides full-modularization and encapsulation of configuration details. Let your architecture tell a story.
- Dependencies declared in any order. (The order that makes sense to humans).
- Makes it easy to have multiple configurations of the same base-class or protocol.
- Supports injection of view controllers and storyboard integration.
- Supports both initializer and property injection, plus life-cycle management.
- Powerful memory management features. Provides pre-configured objects, without the memory overhead of singletons.
- Support for circular dependencies.
- Lean. Has a very low footprint, so is appropriate for CPU and memory constrained devices.
- Weighs-in at just 3000 lines of code in total. Battle-tested — used in all kinds of Appstore-featured apps.
EXAMPLE
You can explore an Objective-C sample of Typhoon here.
CONCLUSION
Dependency Injection is a useful design pattern and it helps to have a supporting library. Today there are some very good pure Swift Dependency Injection libraries, meanwhile, Typhoon is used in over 6000 apps. Some of these include Amex, Accor Hotels, Audible.com, Etihad, easyJet, Grindr, Hootsuite, Lazada, Kobo, Shalom and Singapore Airlines.
Happy coding