Many developers cringe when they hear the words dependency injection. It's a difficult pattern and it's not meant for beginners. That's what you are made to believe. The truth is that dependency injection is a fundamental pattern that is very easy to adopt.
My favorite quote about dependency injection is a quote by James Shore. It summarizes much of the confusion that surrounds dependency injection.
"Dependency Injection" is a 25-dollar term for a 5-cent concept. - James Shore
When I first heard about dependency injection, I also figured it was a technique too advanced for my needs at the time. I could do without dependency injection, whatever it was.
But What Is Dependency Injection
I later learned that, if reduced to its bare essentials, dependency injection is a simple concept. James Shore offers a succinct and straightforward definition of dependency injection.
Dependency injection means giving an object its instance variables. Really. That's it. - James Shore
For developers new to dependency injection, it is important to learn the basics before relying on a framework or library. Start simple. Chances are that you already use dependency injection without realizing it.
Dependency injection is nothing more than injecting dependencies into an object instead of tasking the object with the responsibility of creating its dependencies. Or, as James Shore puts it, you give an object its instance variables instead of creating them in the object. Let me show you what that means with an example.
An Example
In this example, we define a UIViewController
subclass that declares a property, requestManager
, of type RequestManager?
.
import UIKit
class ViewController: UIViewController {
var requestManager: RequestManager?
}
We can set the value of the requestManager
property one of two ways.
Without Dependency Injection
The first option is to task the ViewController
class with the instantiation of the RequestManager
instance. We can make the property lazy or initialize the request manager in the view controller's initializer. That's not the point, though. The point is that the view controller is in charge of creating the RequestManager
instance.
import UIKit
class ViewController: UIViewController {
lazy var requestManager: RequestManager? = RequestManager()
}
This means that the ViewController
class not only knows about the behavior of the RequestManager
class. It also knows about its instantiation. That's a subtle but important detail.
With Dependency Injection
But there's another option. We can inject the RequestManager
instance into the ViewController
instance. Even though the end result may appear identical, it definitely isn't. By injecting the request manager, the view controller doesn't know how to instantiate the request manager.
// Initialize View Controller
let viewController = ViewController()
// Configure View Controller
viewController.requestManager = RequestManager()
Many developers immediately discard this option because it's cumbersome and unnecessarily complex. But if you consider the benefits, dependency injection becomes more appealing.
Another Example
I'd like to show you another example to emphasize the point I made earlier. Take a look at the following example.
protocol Serializer {
func serialize(data: AnyObject) -> NSData?
}
class RequestSerializer: Serializer {
func serialize(data: AnyObject) -> NSData? {
...
}
}
class DataManager {
var serializer: Serializer? = RequestSerializer()
}
The DataManager
class has a property, serializer
, of type Serializer?
. In this example, Serializer
is a protocol. The DataManager
class is in charge of instantiating an instance of a type that conforms to the Serializer
protocol, the RequestSerializer
class in this example.
Should the DataManager
class know how to instantiate an object of type Serializer
? Take a look at this example. It shows you the power of protocols and dependency injection.
// Initialize Data Manager
let dataManager = DataManager()
// Configure Data Manager
dataManager.serializer = RequestSerializer()
The DataManager
class is no longer in charge of instantiating the RequestSerializer
class. It no longer assigns a value to its serializer
property. In fact, we can replace RequestSerializer
with another type as long as it conforms to the Serializer
protocol. The DataManager
no longer knows or cares about these details.
What Do You Gain
I hope that the examples I showed you have at least captured your attention. Let me list a few additional benefits of dependency injection.
Transparency
By injecting the dependencies of an object, the responsibilities and requirements of a class or structure become more clear and more transparent. By injecting a request manager into a view controller, we understand that the view controller depends on the request manager and we can assume that the view controller is responsible for request managing and/or handling.
Testing
Unit testing is so much easier with dependency injection. Dependency injection makes it very easy to replace an object's dependencies with mock objects, making unit tests easier to set up and isolate behavior.
In this example, we define a class, MockSerializer
. Because it conforms to the Serializer
protocol, we can assign it to the data manager's serializer
property.
class MockSerializer: Serializer {
func serialize(data: AnyObject) -> NSData? {
...
}
}
// Initialize Data Manager
let dataManager = DataManager()
// Configure Data Manager
dataManager.serializer = MockSerializer()
Separation of Concerns
As I mentioned and illustrated earlier, another subtle benefit of dependency injection is a stricter separation of concerns. The DataManager
class in the previous example isn't responsible for instantiating the RequestSerializer
instance. It doesn't need to know how to do this.
Even though the DataManager
class is concerned with the behavior of its serializer, it isn't, and shouldn't be, concerned with its instantiation. What if the RequestManager
of the first example also has a number of dependencies. Should the ViewController
instance be aware of those dependencies too? This can become very messy very quickly.
Coupling
The example with the DataManager
class illustrated how the use of protocols and dependency injection can reduce coupling in a project. Protocols are incredibly useful and versatile in Swift. This is one scenario in which protocols really shine.
Types
Most developers consider three forms or types of dependency injection:
- initializer injection
- property injection
- method injection
These types shouldn't be considered equal, though. Let me list the pros and cons of each type.
Initializer Injection
I personally prefer to pass dependencies during the initialization phase of an object because this has several key benefits. The most important benefit is that dependencies passed in during initialization can be made immutable. This is very easy to do in Swift by declaring the properties for the dependencies as constants. Take a look at this example.
class DataManager {
private let serializer: Serializer
init(serializer: Serializer) {
self.serializer = serializer
}
}
// Initialize Request Serializer
let serializer = RequestSerializer()
// Initialize Data Manager
let dataManager = DataManager(serializer: serializer)
The only way to set the serializer
property is by passing it as an argument during initialization. The init(serializer:)
method is the designated initializer and guarantees that the DataManager
instance is correctly configured. Another benefit is that the serializer
property cannot be mutated.
Because we are required to pass the serializer as an argument during initialization, the designated initializer clearly shows what the dependencies of the DataManager
class are.
Property Injection
Dependencies can also be injected by declaring an internal or public property on the class or structure that requires the dependency. This may seem convenient, but it adds a loophole in that the dependency can be modified or replaced. In other words, the dependency isn't immutable.
import UIKit
class ViewController: UIViewController {
var requestManager: RequestManager?
}
// Initialize View Controller
let viewController = ViewController()
// Configure View Controller
viewController.requestManager = RequestManager()
Property injection is sometimes the only option you have. If you use storyboards, for example, you cannot implement a custom initializer and use initializer injection. Property injection is then your next best option.
Method Injection
Dependencies can also be injected whenever they are needed. This is easy to do by defining a method that accepts the dependency as a parameter. In this example, the serializer isn't a property on the DataManager
class. Instead, the serializer is injected as an argument of the serializeRequest(_:with:)
method.
class DataManager {
func serializeRequest(request: Request, with serializer: Serializer) -> NSData? {
...
}
}
Even though the DataManager
class loses some control over the dependency, the serializer, this type of dependency injection introduces flexibility. Depending on the use case, we can choose what type of serializer to pass into serializeRequest(_:with:)
.
It's important to emphasize that each type of dependency injection has its use cases. While initializer injection is a great option in many scenarios, that doesn't make it best or preferred type. Consider the use case and then decide which type of dependency injection is the best fit.
Singletons
Dependency injection is a pattern that can be used to eliminate the need for singletons in a project. I'm not a fan of the singleton pattern and I avoid it whenever possible. Even though I don't consider the singleton pattern an anti-pattern, I believe they should be used very, very sparingly. The singleton pattern increases coupling whereas dependency injection reduces coupling.
Too often, developers use the singleton pattern because it's an easy solution to a, often trivial, problem. Dependency injection, however, adds clarity to a project. By injecting dependencies during the initialization of an object, it becomes clear what dependencies the target class or structure has and it also reveals some of the object's responsibilities.
Dependency injection is one of my favorite patterns because it helps me stay on top of complex projects. This pattern has so many benefits. The only drawback I can think of is the need for a few more lines of code.