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
.
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
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
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.
To invert the dependency, we define NewsLoader
inside the same module that has NewsViewController
.
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 in my website abdullahth.com.
Top comments (0)