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.