Dependency injection means providing a class' dependencies in a way that allows them to be easily substituted for something else.
A classic example of this is in practice is injecting classes that need to make networking requests with a NetworkManager
. Often, when running tests, this class will not actually communicate with the network, so we substitute it with a NetworkManagerMock
.
How does this work in practice? The word injection can be confusing, because it's unclear what it actually means to "inject" a dependency into a class. But this often just means passing dependency in when the class is initialized:
class MyClass {
init(networkManager: NetworkManager) { ... }
}
The next key part is the ability to substitute this dependency with other instances. In Swift, this is accomplished with protocols. Instead of passing in an instance of NetworkManager
, we instead just initialize our class with something that conforms to the NetworkManagerProtocol
.
protocol NetworkManagerProtocol {
func load(_ url: URL,
completion: ((Response) -> Void)?)
}
class MyClass {
init(networkManager: NetworkManagerProtocol) { ... }
}
In this way, we easily swap out our NetworkManager
for another class with a different implementation, provided it conforms to the protocol. This provides a lot of flexibility. For example, we could migrate to a different networking library under the hood, without needing to change any of the classes that make network requests.
We can also quickly implement mocks for testing:
class NetworkManagerMock: NetworkManagerProtocol {
func load(_ url: URL,
completion: ((Response) -> Void)?) {
return
}
}
I've chosen to just return
in the implementation, but we could hypothetically pass a mock Response
object to our completion
block if it were needed for testing.