Dependency Injection

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.