Dependency Injection: Give Your iOS Code a Shot in the Arm
Applying the dependency injection design pattern to Objective-C or Swift codebases.
Written by Eric Firestone.
What Is Dependency Injection?
Dependency injection (DI) is a popular design pattern in many languages, such as Java and C# , but it hasn’t seen wide adoption in Objective-C. This article aims to give a brief introduction to dependency injection using Objective-C examples, as well as practical methods for using dependency injection in Objective-C code. Although the article focuses on Objective-C, all of the concepts apply to Swift as well.
The concept of dependency injection is very simple: an object should require you to pass in any dependencies rather than creating them itself. Martin Fowler’s excellent discussion on the subject is highly suggested background reading.
Dependencies can be passed to an object via the initializer (or “constructor”), or via properties (or “setters”). These are commonly referred to as “constructor injection” and “setter injection.”
Constructor Injection:
- (**instancetype**)**initWithDependency1:**(Dependency1 *****)d1
**dependency2:**(Dependency2 *****)d2;
Setter Injection:
**@property** (**nonatomic**, **retain**) Dependency1 *****dependency1;
**@property** (**nonatomic**, **retain**) Dependency2 *****dependency2;
As Fowler describes, constructor injection is preferred, and as a general rule you should only fall back to setter injection if constructor injection is not possible. With constructor injection, you’ll likely still have @property definitions for these dependencies, but you can make them read only to simplify your object’s API.
Why Use Dependency Injection?
Dependency injection offers a number of benefits, but some of the more important ones are:
-
Clear declaration of dependencies It becomes obvious what an object needs in order to operate, and dangerous hidden dependencies — like globals — disappear.
-
Composition DI encourages composition over inheritance, which improves the reusability of your code.
-
Easy customization When creating an object, it’s easy to customize parts of the object for specific scenarios.
-
Clear ownership Particularly when using constructor injection, the object ownership rules are strictly enforced — helping to build a directed acyclic object graph.
-
Testability More than anything else, dependency injection improves the testability of your objects. Because they can be created simply by filling in the initializer, no hidden dependencies need to be managed. Furthermore, it becomes simple to mock out the dependencies to focus your tests on the object being tested.
Switching to Dependency Injection
Your codebase may not already be designed using the dependency injection design pattern, but it’s easy to get started. One nice aspect of dependency injection is that you don’t need to adopt “all or nothing.” Instead, you can apply it to particular areas of the codebase and expand from there.
Types of Classes to Inject
First, let’s classify classes into two buckets: basic classes and complex classes. Basic classes are ones which have no dependencies, or which depend only on other basic classes. Basic classes are highly unlikely to be subclassed, because their functionality is clear, invariable, and doesn’t reference external resources. Many examples of basic classes come from Cocoa itself, such as NSString, NSArray, NSDictionary, and NSNumber.
Complex classes are the opposite. They have other complex dependencies, include application-level logic (which may need to change), or access external resources such as the disk, the network, or a global in-memory service. Most of the classes in your application will be complex, including almost any controller object and most model objects. Many Cocoa classes are complex as well, such as NSURLConnection or UIViewController.
Given these classifications, the easiest way to get started is to pick a complex class in your application and look for places where you initialize other complex objects within that class (search for “alloc] init” or “new]”). To introduce dependency injection into the class, change this instantiated object to be passed in as an initializer parameter on the class rather than the class instantiating the object itself.
Dependencies Allocated During Initialization
Let’s look at an example where the child object (the dependency) is being initialized as part of the parent’s initialization. The original code might look something like this:
**@interface** **RCRaceCar** ()
@property (nonatomic, readonly) RCEngine *engine;
@end
@implementation RCRaceCar
- (instancetype)init
{
...
// Create the engine. Note that it cannot be customized or
// mocked out without modifying the internals of RCRaceCar.
_engine = [[RCEngine alloc] init];
return self;
}
@end
The dependency injection version of this is a simple change:
**@interface** **RCRaceCar** ()
@property (nonatomic, readonly) RCEngine *engine;
@end
@implementation RCRaceCar
// The engine is created before the race car and passed in
// as a parameter, and the caller can customize it if desired.
- (instancetype)initWithEngine:(RCEngine *)engine
{
...
_engine = engine;
return self;
}
@end
Lazily Initialized Dependencies
Some objects aren’t needed until a later time, after initialization, or may never be needed at all. An example (before using dependency injection) might look like this:
**@interface** **RCRaceCar** ()
@property (nonatomic) RCEngine *engine;
@end
@implementation RCRaceCar
- (instancetype)initWithEngine:(RCEngine *)engine
{
...
_engine = engine;
return self;
}
- (void)recoverFromCrash
{
if (self.fire != nil) {
RCFireExtinguisher *fireExtinguisher = [[RCFireExtinguisher alloc] init];
[fireExtinguisher extinguishFire:self.fire];
}
}
@end
In this case the race car hopefully never crashes, and we never need to use our fire extinguisher. Because the chance of needing this object is low, we don’t want to slow down the creation of every race car by creating it immediately in initialization. Alternatively, if our race car needs to recover from multiple crashes, it will need to create multiple extinguishers. For these situations, we can use a factory.
Factories are standard Objective-C blocks that require no arguments and return a concrete instance of an object. An object can control when its dependencies are created using these blocks without needing to know the details of how to create them.
Here’s an example using dependency injection that uses a factory to create our fire extinguisher:
**typedef** RCFireExtinguisher *****(**^**RCFireExtinguisherFactory)();
@interface RCRaceCar ()
@property (nonatomic, readonly) RCEngine *engine;
@property (nonatomic, copy, readonly) RCFireExtinguisherFactory fireExtinguisherFactory;
@end
@implementation RCRaceCar
- (instancetype)initWithEngine:(RCEngine *)engine
fireExtinguisherFactory:(RCFireExtinguisherFactory)extFactory
{
...
_engine = engine;
_fireExtinguisherFactory = [extFactory copy];
return self;
}
- (void)recoverFromCrash
{
if (self.fire != nil) {
RCFireExtinguisher *fireExtinguisher = self.fireExtinguisherFactory();
[fireExtinguisher extinguishFire:self.fire];
}
}
@end
Factories are also useful in cases where we need to create an unknown number of a dependency, even if that creation is done during initialization. For example:
**@implementation** **RCRaceCar**
- (instancetype)initWithEngine:(RCEngine *)engine
transmission:(RCTransmission *)transmission
wheelFactory:(RCWheel *(^)())wheelFactory;
{
self = [super init];
if (self == nil) {
return nil;
}
_engine = engine;
_transmission = transmission;
_leftFrontWheel = wheelFactory();
_leftRearWheel = wheelFactory();
_rightFrontWheel = wheelFactory();
_rightRearWheel = wheelFactory();
// Keep the wheel factory for later in case we need a spare.
_wheelFactory = [wheelFactory copy];
return self;
}
@end
Avoiding Cumbersome Configuration
If objects shouldn’t be allocated within other objects, where are they allocated? And aren’t all these dependencies hard to configure? Aren’t most of them the same every time? The solution to these problems lies in class convenience initializers (think +[NSDictionary dictionary]). We’ll pull the configuration of our object graph out of our normal objects, leaving them with pure, testable, business logic.
Before adding a class convenience initializer, make sure that it’s necessary. If an object has only a few arguments to its init method, and those arguments don’t have reasonable defaults, then a class convenience initializer isn’t necessary and the caller should use the standard init method directly.
To configure our object we’ll gather our dependencies from four places:
-
Values without a reasonable default These include booleans or numbers that will likely be different for each instance. These values should be passed into the class convenience initializer as arguments.
-
Existing shared objects These should be passed into the class convenience initializer as arguments (such as a pit radio frequency, for example). These are the objects that previously might have been assessed as singletons or via “parent” pointers.
-
Newly created objects If our object does not share this dependency with another object, then a collaborator object should be newly instantiated in the class convenience initializer. These are objects that previously were allocated directly within the implementation of the object.
-
System singletons These are Cocoa-provided singletons and can be accessed directly from the singleton. This applies to singletons such as [NSFileManager defaultManager], where there is a reasonable expectation that only one instance will ever be used in your production app. There is more on system singletons below.
A class convenience initializer for our race car would look like this:
+ (**instancetype**)**raceCarWithPitRadioFrequency:**(RCRadioFrequency *****)frequency;
{
RCEngine *****engine **=** [[RCEngine alloc] init];
RCTransmission *****transmission **=** [[RCTransmission alloc] init];
RCWheel *(^wheelFactory)() = ^{
return [[RCWheel alloc] init];
};
return [[self alloc] initWithEngine:engine
transmission:transmission
pitRadioFrequency:frequency
wheelFactory:wheelFactory];
}
Your class convenience initializer should live where it feels most appropriate. A commonly used (and reusable) configuration will live in the same .m file as the object, while a configuration that is used specifically by a Foo object should live in a @interface RaceCar (FooConfiguration) category and be named something like fooRaceCar.
System Singletons
For many objects in Cocoa, only one instance will ever exist. Examples include [UIApplication sharedApplication], [NSFileManager defaultManager], [NSUserDefaults standardUserDefaults], and [UIDevice currentDevice]. If an object has a dependency on one of these objects, then it should be included as an initializer argument. Even though there may only be one instance in your production code, your tests may want to mock that instance out or create one instance per-test to avoid test interdependence.
It’s recommended that you avoid creating globally-referenced singletons in your own code, and instead create a single instance of an object when it is first needed and inject it into all the objects that depend on it.
Unmodifiable Constructors
Occasionally there are cases where the initializer/constructor for a class can’t be changed or can’t be called directly. In these cases you should use setter injection. For example:
*// An example where we can't directly call the the initializer.*
RCRaceTrack *****raceTrack **=** [objectYouCantModify createRaceTrack];
// We can still use properties to configure our race track.
raceTrack.width = 10;
raceTrack.numberOfHairpinTurns = 2;
Setter injection allows you to configure the object, but it introduces additional mutability that must be tested and handled in the object’s design. Luckily, there are two primary scenarios that cause initializers to be inaccessible or unmodifiable, and both can be avoided.
Class Registration
The use of the “class registration” factory pattern means that objects cannot modify their initializers.
NSArray *****raceCarClasses **=** @[
[RCFastRaceCar **class**],
[RCSlowRaceCar **class**],
];
NSMutableArray *raceCars = [[NSMutableArray alloc] init];
for (Class raceCarClass in raceCarClasses) {
// All race cars must have the same initializer ("init" in this case).
// This means we can't customize different subclasses in different ways.
[raceCars addObject:[[raceCarClass alloc] init]];
}
An easy replacement for this is to use factory blocks rather than classes to declare your list.
**typedef** RCRaceCar *****(**^**RCRaceCarFactory)();
NSArray *raceCarFactories = @[
^{ return [[RCFastRaceCar alloc] initWithTopSpeed:200]; },
^{ return [[RCSlowRaceCar alloc] initWithLeatherPlushiness:11]; }
];
NSMutableArray *raceCars = [[NSMutableArray alloc] init];
for (RCRaceCarFactory raceCarFactory in raceCarFactories) {
// We now no longer care which initializer is being called.
[raceCars addObject:raceCarFactory()];
}
Storyboards
Storyboards offer a convenient way to lay out your user interface, but they also introduce problems when it comes to dependency injection. In particular, instantiating the initial view controller in a storyboard does not allow you to choose which initializer is called. Similarly, when following a segue that is defined in a storyboard, the destination view controller is instantiated for you without letting you specify the initializer.
The solution here is to avoid using storyboards. This seems like an extreme solution, but we’ve found that storyboards have other issues scaling with large teams. Additionally, there’s no need to lose most of the benefit of storyboards. XIBs offer all of the same benefits that storyboards provide, minus segues, but still let you customize the initializer.
Public vs. Private
Dependency injection encourages you to expose more objects in your public interface. As mentioned, this has numerous benefits. But when building frameworks, it can significantly bloat your public API. Prior to applying dependency injection, public Object A may have used private Object B (which in turn uses private Object C), but Objects B and C were never exposed outside of the framework. With dependency injection Object A has Object B in its public initializer, and Object B in turn makes Object C public in its initializer.
*// In public ObjectA.h.*
**@interface** **ObjectA**
*// Because the initializer uses a reference to ObjectB we need to*
*// make the Object B header public where we wouldn't have before.*
- (**instancetype**)**initWithObjectB:**(ObjectB *****)objectB;
**@end**
@interface ObjectB
// Same here: we need to expose ObjectC.h.
- (instancetype)initWithObjectC:(ObjectC *)objectC;
@end
@interface ObjectC
- (instancetype)init;
@end
Object B and Object C are implementation details, and you don’t want the framework consumer to have to worry about them. We can solve this by using protocols.
**@interface** **ObjectA**
- (**instancetype**)**initWithObjectB:**(**id** **<**ObjectB**>**)objectB;
**@end**
// This protocol exposes only the parts of the original ObjectB that
// are needed by ObjectA. We're not creating a hard dependency on
// our concrete ObjectB (or ObjectC) implementation.
@protocol ObjectB
- (void)methodNeededByObjectA;
@end
Closing
Dependency injection is a natural fit for Objective-C (and Swift by extension). Applied properly it makes your codebase easy to read, easy to test, and easy to maintain. Eric Firestone (@firetweet) | Twitter *The latest Tweets from Eric Firestone (@firetweet): "Congrats @segiddins and @CocoaPods team for reaching the 1.0…*twitter.com
Authored By
- What Is Dependency Injection?
- Why Use Dependency Injection?
- Switching to Dependency Injection
- Types of Classes to Inject
- Dependencies Allocated During Initialization
- Lazily Initialized Dependencies
- Avoiding Cumbersome Configuration
- System Singletons
- Unmodifiable Constructors
- Class Registration
- Storyboards
- Public vs. Private
- Closing