Why Dependency Injection?

Why Dependency Injection?

In the previous post , we discussed the what of DI. In this post, we will explore the why.

DI enables many benefits; before discussing those benefits, let us establish an example. let's say we have a view controller that can display news:

class NewsViewController: UIViewController {
    let newsLoader: NewsLoader

    init(newsLoader: NewsLoader) {
        self.newsLoader = newsLoader
        super.init(nibName: nil, bundle: nil)
    }
}

The NewsViewController uses DI because it doesn't create a NewsLoader. Instead, it states that it needs a NewsLoader. The NewsLoader is defined as a protocol:

protocol NewsLoader {
    func load(completion: (Result<[News], Error>) -> Void)
}

And we have two types that implement the NewsLoader protocol. One fetches the news from the network. The other fetches the news from a database:

class APINewsLoader: NewsLoader {
    func load(completion: (Result<[News], Error>) -> Void) {
        // load news from the network
    }
}

class DatabaseNewsLoader: NewsLoader {
    func load(completion: (Result<[News], Error>) -> Void) {
        // load news from a local database
    }
}

So you can use the same NewsViewController to display news from different sources. If you want to fetch news from the network, you use:

let apiNewsLoader = APINewsLoader()
let newsViewController = NewsViewController(newsLoader: apiNewsLoader)

If you want to display news from a database, you use:

let databaseNewsLoader = DatabaseNewsLoader()
let newsViewController = NewsViewController(newsLoader: databaseNewsLoader)

With this example established, let's explore the benefits of DI.

Late binding

Late binding basically means that you bind the dependency later. That is, you decide which dependency to use at run time instead of compile time. For example, say we want to display news from the network only if the device is online. If the device is offline, we will display news from the database instead:

if Network.isOnline {
    let newsViewController = NewsViewController(newsLoader: apiNewsLoader)
} else {
    let newsViewController = NewsViewController(newsLoader: databaseNewsLoader)
}

Dependency Inversion

If our NewsViewController doesn't use DI and creates its news loader dependency directly like so:

class NewsViewController: UIViewController {
    let newsLoader = APINewsLoader()
}

Then we can say our NewsViewController directly depends on APINewsLoader.

Blank diagram - Direct Dependency.png

However, when NewsViewController doesn't depend on APINewsLoader but depends on NewsLoader. Then in order to make APINewsLoader work with NewsViewController we need to confirm it to the NewsLoader protocol. So we inverted the dependency

Blank diagram - Dependency Inversion.png

Note that instead of the arrow going from NewsViewController to APINewLoader, the arrow goes back from APINewsLoader to NewsLoader, hence the name dependency inversion.

It is worth noting that NewsLoader should be owned by the module (or layer) that owns the NewsViewController. If not, then NewsViewController still has a direct dependency to another module/layer.

If we re-draw the previous diagram between the modules/layers instead of the individual classes, the dependency inversion concept will be more apparent.

So, without using NewsLoader to invert the dependency, we will have a direct dependency between the module that has NewsViewController and the module that has APINewsLoader

Blank diagram - Copy of Direct Dependency-2.png

If we define NewsLoader inside the same module that has APINewsLoader, then we still have a direct dependency from the first module to the second one.

Blank diagram - Incorrect.png

To invert the dependency, we define NewsLoader inside the same module that has NewsViewController.

Blank diagram - Copy of Copy of Direct Dependency.png

Dependency inversion is an essential technique in architecture, where we might want to protect some modules from depending on others. Typically, we want our application code not to depend on frameworks.

In our example, APINewsLoader indeed uses some framework to do the actual networking calls. Since NewsViewController doesn't directly depend on it, we can change or replace that framework without needing to change our UI.

Testability

To unit test the NewsViewController, we want to focus on the NewsViewController behavior itself, and not any collaborating objects. However, we cannot run NewsViewController without a NewsLoader. Since we are using DI to inject NewsLoader, we don't have to inject a real implementation such as APINewsLoader or DatabaseNewsLoader. Instead, we can define a mock implementation of NewsLoader

class MockNewsLoader: NewsLoader {
    func load(completion: (Result<[News], Error>) -> Void) {
        // No need to hit the real network or call the real database
        // you can return any news you want here to use in testing
    }
}

class NewsViewControllerTests: XCTestCase {
    func test_theThingYouWantToTest() {
        let mockNewsLoader = MockNewsLoader()
        let newsViewController = NewsViewController(newsLoader: mockNewsLoader)

        // test code here ...
    }
}

Extensibility

Let's say that you want to load news related to Apple only, and you want to reuse the same NewsViewController to display Apple news.

The two classes we defined above, APINewsLoader and DatabaseNewsLoader, return all news. We don't have to write, for example, APIAppleNewsLoader and AppleDatabaseNewsLoader and implement the same behavior except filtering the news. That is too much unnecessary work.

With proper DI, we can implement this feature without changing existing ones. We can define AppleNewsLoader that implements the same NewsLoader and inject into it another NewsLoader. So we can reuse previous implementations and add on them (extent) new ones.

class AppleNewsLoader: NewsLoader {
    let newsLoader: NewsLoader

    init(newsLoader: NewsLoader) {
        self.newsLoader = newsLoader
    }

    func load(completion: (Result<[News], Error>) -> Void) {
        newsLoader.load { result in
            switch result {
            case .success(let news):
                let appleNews = getAppleNews(from: news)
                completion(.success(appleNews))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }

    private func getAppleNews(from: [News]) -> [News] {
        // filter and return Apple news ...
    }
}

To use our new AppleNewsLoader with news either from the network or the database, depending on the status of the device network, we can construct our objects as follow:

let newsLoader: NewsLoader = Network.isOnline ? APINewsLoader() : DatabaseNewsLoader()
let appleNewsLoader = AppleNewsLoader(newsLoader: newsLoader)
let newsViewController = NewsViewController(newsLoader: appleNewsLoader)

As you can see, DI enables us to compose and extent our objects in unprecedented ways!

Conclusion

There are many benefits to DI. We only discussed the main ones. These primary benefits also, in turn, enable other benefits. For example, testability enables maintainability, and dependency inversion enables loose-coupling and parallel development.

I will continue to write about DI in my blog, and I'm planning to do a complete sample project. So if you are interested, please subscribe to my newsletter.