Even though dependency injection is a topic that is rarely taught to beginners, it is a design pattern that deserves more attention. Many developers avoid dependency injection, because they don't know what it means or because they think that they don't need it.
In this article, I'm going to try to convince you of the value of dependency injection. To do this, I will introduce you to dependency injection by showing you how simple it is in its simplest form.
1. What Is Dependency Injection?
Much has been written about dependency injection and there are a bunch of tools and libraries that aim to simplify dependency injection. There is one quote, however, that captures the confusion many people have around dependency injection.
"Dependency Injection" is a 25-dollar term for a 5-cent concept. - James Shore
Once you grasp the idea that underlies dependency injection, you will also understand the above quote. Let's start with an example to illustrate the concept.
An iOS application has many dependencies and your application may rely on dependencies you're not even aware of, that is, you don't consider them dependencies. The following code snippet shows the implementation of a UIViewController
subclass named ViewController
. The implementation includes a method named saveList:
. Can you spot the dependency?
#import "ViewController.h" @interface ViewController () @end @implementation ViewController - (void)saveList:(NSArray *)list { if ([list isKindOfClass:[NSArray class]]) { NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; [userDefaults setObject:list forKey:@"list"]; } } @end
The most commonly overlooked dependencies are the ones that we rely on the most. In the saveList:
method, we store an array in the user defaults database, accessible through the NSUserDefaults
class. We access the shared defaults object by invoking the standardUserDefaults
method. If you're somewhat familiar with iOS or OS X development, then you're likely to be familiar with the NSUserDefaults
class.
Storing data in the user defaults database is fast, easy, and reliable. Thanks to the standardUserDefaults
method, we have access to the user defaults database from anywhere in the project. The method returns a singleton that we can use whenever and wherever we want. Life can be beautiful.
Singleton? Whenever and wherever? Do you smell something? Not only do I smell a dependency, I also smell a bad practice. In this article, I don't want to open up a can of worms by discussing the use and misuse of singletons, but it is important to understand that singletons should be used sparingly.
Most of us have become so used to the user defaults database that we don't see it as a dependency. But it certainly is one. The same is true for the notification center, which we commonly access through the singleton accessible through the defaultCenter
method. Take a look at the following example for clarification.
#import "ViewController.h" @interface ViewController () @end @implementation ViewController #pragma mark - #pragma mark Initialization - (instancetype)init { self = [super init]; if (self) { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; } return self; } #pragma mark - #pragma mark Memory Management - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - #pragma mark Notification Handling - (void)applicationWillEnterForeground:(NSNotification *)notification { // ... } @end
The above scenario is very common. We add the view controller as an observer for notifications with name UIApplicationWillEnterForegroundNotification
and remove it as an observer in the dealloc
method of the class. This adds another dependency to the ViewController
class, a dependency that is often overlooked—or ignored.
The question you may be asking yourself is "What is the problem?" or better "Is there a problem?" Let's start with the first question.
What Is the Problem?
Based on the above examples, it may seem as if there is no problem. That isn't entirely true though. The view controller depends on the shared defaults object and the default notification center to do its work.
Is that a problem? Almost every object relies on other objects to do its work. The issue is that the dependencies are implicit. A developer new to the project doesn't know the view controller relies on these dependencies by inspecting the class interface.
Testing the ViewController
class will also prove to be tricky since we don't control the NSUserDefaults
and NSNotificationCenter
classes. Let's look at some solutions to this problem. In other words, let's see how dependency injection can help us solve this problem.
2. Injecting Dependencies
As I mentioned in the introduction, dependency injection is a very simple concept. James Shore has written a great article about the simplicity of dependency injection. There is one other quote from James Shore about what dependency injection is at its very core.
Dependency injection means giving an object its instance variables. Really. That's it. - James Shore
There are a number of ways to accomplish this, but it's important to first understand what the above quote means. Let's see how we can apply this to the ViewController
class.
Instead of accessing the default notification center in the init
method through the defaultCenter
class method, we create a property for the notification center in the ViewController
class. This is what the updated interface of the ViewController
class looks like after this addition.
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (weak, nonatomic) NSNotificationCenter *defaultCenter; @end
This also means that we need to do a bit of extra work when we initialize an instance of the ViewController
class. As James writes, we hand the ViewController
instance its instance variables. That is how simple dependency injection is. It's a fancy name for a straightforward concept.
// Initialize View Controller ViewController *viewController = [[ViewController alloc] init]; // Configure View Controller [viewController setNotificationCenter:[NSNotificationCenter defaultCenter]];
As a result of this change, the implementation of the ViewController
class changes. This is what the init
and dealloc
methods look like when injecting the default notification center.
#pragma mark - #pragma mark Initialization - (instancetype)init { self = [super init]; if (self) { [self.notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; } return self; } #pragma mark - #pragma mark Memory Management - (void)dealloc { [_notificationCenter removeObserver:self]; }
Note that we don't use self
in the dealloc
method. This is considered a bad practice since it may lead to unexpected results.
There is one problem. Can you spot it? In the initializer of the ViewController
class, we access the notificationCenter
property to add the view controller as an observer. During initialization, however, the notificationCenter
property hasn't been set yet. We can solve this problem by passing the dependency as a parameter of the initializer. This is what that looks like.
#pragma mark - #pragma mark Initialization - (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter { self = [super init]; if (self) { // Set Notification Center [self setNotificationCenter:notificationCenter]; // Add Observer [self.notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; } return self; }
To make this work, we also need to update the interface of the ViewController
class. We omit the notificationCenter
property and add a method declaration for the initializer we created.
#import <UIKit/UIKit.h> @interface ViewController : UIViewController #pragma mark - #pragma mark Initialization - (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter; @end
In the implementation file, we create a class extension in which we declare the notificationCenter
property. By doing so, the notificationCenter
property is private to the ViewController
class and can only be set by invoking the new initializer. This is another best practice to keep in mind, only expose properties that need to be public.
#import "ViewController.h" @interface ViewController () @property (strong, nonatomic) NSNotificationCenter *notificationCenter; @end
To instantiate an instance of the ViewController
class, we rely on the initializer we created earlier.
// Initialize View Controller ViewController *viewController = [[ViewController alloc] initWithNotificationCenter:[NSNotificationCenter defaultCenter]];
3. Benefits
What have we accomplished by explicitly injecting the notification center object as a dependency?
Clarity
The interface of the ViewController
class unequivocally shows that the class relies or depends on the NSNotificationCenter
class. If you're new to iOS or OS X development, this may seem like a small win for the complexity we added. However, as your projects become more complex, you will learn to appreciate every bit of clarity you can add to a project. Explicitly declaring dependencies will help you with this.
Modularity
When you start using dependency injection, your code will become much more modular. Even though we injected a specific class into the ViewController
class, it is possible to inject an object that conforms to a specific protocol. If you adopt this approach, it will become much easier to replace one implementation with another.
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (strong, nonatomic) id<MyProtocol> someObject; #pragma mark - #pragma mark Initialization - (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter; @end
In the above interface of the ViewController
class, we declare another dependency. The dependency is an object that conforms to the MyProtocol
protocol. This is where the true power of dependency injection becomes apparent. The ViewController
class doesn't care about the type of someObject
, it only asks that it adopts the MyProtocol
protocol. This makes for highly modular, flexible, and testable code.
Testing
Even though testing isn't as widespread among iOS and OS X developers as it is in other communities, testing is an key topic that gains in importance and popularity. By adopting dependency injection, you will make your code much easier to test. How would you test the following initializer? That's going to be tricky. Right?
#pragma mark - #pragma mark Initialization - (instancetype)init { self = [super init]; if (self) { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; } return self; }
The second initializer, however, makes this task much easier. Take a look at the initializer and the test that goes with it.
#pragma mark - #pragma mark Initialization - (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter { self = [super init]; if (self) { // Set Notification Center [self setNotificationCenter:notificationCenter]; // Add Observer [self.notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; } return self; }
#pragma mark - #pragma mark Tests for Initialization - (void)testInitWithNotificationCenter { // Create Mock Notification Center id mockNotificationCenter = OCMClassMock([NSNotificationCenter class]); // Initialize View Controller ViewController *viewController = [[ViewController alloc] initWithNotificationCenter:mockNotificationCenter]; XCTAssertNotNil(viewController, @"The view controller should not be nil."); OCMVerify([mockNotificationCenter addObserver:viewController selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]); }
The above test makes use of the OCMock library, an excellent mocking library for Objective-C. Instead of passing in an instance of the NSNotificationCenter
class, we pass in a mock object and verify whether the methods that need to be invoked in the initializer are indeed invoked.
There are several approaches to test notification handling and this is—by far—the easiest I've come across. It adds a bit of overhead by injecting the notification center object as a dependency, but the benefit outweighs the added complexity in my opinion.
4. Third Party Solutions
I hope I've convinced you that dependency injection is a simple concept with a simple solution. There are, however, a number of popular frameworks and libraries that aim to make dependency injection more powerful and easier to manage for complex projects. The two most popular libraries are Typhoon and Objection.
If you're new to dependency injection, then I strongly recommend to start using the techniques outlined in this tutorial. You first need to properly understand the concept before relying on a third party solution, such as Typhoon or Objection.
Conclusion
The goal of this article was to make dependency injection easier to understand for people who are new to programming and unfamiliar with the concept. I hope I have convinced you of the value of dependency injection and the simplicity of the underlying idea.
There are a number of excellent resources about dependency injection. James Shore's article about dependency injection is a must-read for every developer. Graham Lee also wrote a great article aimed at iOS and OS X developers.
Comments