Dependency injection is a programming technique that makes a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation. This helps you to follow SOLID’s dependency inversion and single responsibility principles.
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.
Dependency Injection (DI) is a technique which allows to populate a class with objects, rather than relying on the class to create the objects itself.
As Robert Martin mentions, dependency injection cannot be considered without Dependency inversion principle. The principle states that implementation details should depend on and implement higher level abstractions, rather than the other way around. It is foundational when creating loosely-coupled applications, which aligns with Mark Seemann’s definition.
Martin Fowler describes DI from the implementation standpoint: a class depends on an interface, having an implementation supplied from the outside. This highlights three actors, involved in dependency injection:
After understanding the concept of dependency injection, let’s see how it is implemented in Swift.
There are four ways how client can receive a dependency:
Let’s study how each option is implemented in Swift and which pros and cons does it have.
Description: dependencies are passed via initializer.
When to use: whenever possible. Fits best when the number of dependencies is low or the object needs to be immutable.
Implementation:
protocol Dependency {
func foo()
}
struct DependencyImplementation: Dependency {
func foo() {
// Does something
}
}
class Client {
let dependency: Dependency
init(dependency: Dependency) {
self.dependency = dependency
}
func foo() {
dependency.foo()
}
}
let client = Client(dependency: DependencyImplementation())
client.foo()
Description: dependencies are passed via properties.
When to use: dependencies need to be changed later or you do not directly initialize the object. View controllers and NSManagedObject
are examples of the latter.
Implementation:
protocol Dependency {
func foo()
}
struct DependencyImplementation: Dependency {
func foo() {
// Does something
}
}
class Client {
var dependency: Dependency!
func foo() {
dependency.foo()
}
}
let client = Client()
client.dependency = DependencyImplementation()
client.foo()
Description: dependency is injected via setter method or passed as a parameter.
When to use: different types of clients need to be handled by an injector. Allows injector to apply policies over the clients.
Implementation:
protocol Dependency {}
protocol HasDependency {
func setDependency(_ dependency: Dependency)
}
protocol DoesSomething {
func doSomething()
}
class Client: HasDependency, DoesSomething {
private var dependency: Dependency!
func setDependency(_ dependency: Dependency) {
self.dependency = dependency
}
func doSomething() {
// Does something with a dependency
}
}
class Injector {
typealias Client = HasDependency & DoesSomething
private var clients: [Client] = []
func inject(_ client: Client) {
clients.append(client)
client.setDependency(SomeDependency())
// Dependency applies its policies over clients
client.doSomething()
}
// Switch dependencies under certain conditions
func switchToAnotherDependency() {
clients.forEach { $0.setDependency(AnotherDependency()) }
}
}
class SomeDependency: Dependency {}
class AnotherDependency: Dependency {}
In the above example Injector
handles any client, conforming to HasDependency
and DoesSomething
protocols. For this pattern to be useful, injector needs to apply certain policies over its clients. In our case, it calls doSomething()
and is capable of switching dependencies.
Description: single globally accessible dependency, exposed via protocol. This allows to substitute implementation if needed, e.g. in tests.
When to use: system-wide dependency, used by dozens of clients. Instead of injecting it to so many clients, we create single and globally accessible dependency.
Implementation:
protocol DateTimeProvider {
var now: Date { get }
}
struct SystemDateTimeProvider: DateTimeProvider {
var now: Date {
return Date()
}
}
class DateTime {
static var provider: DateTimeProvider = SystemDateTimeProvider()
static var now: Date {
return provider.now
}
}
This way we can change DateTimeProvider
to use server time or control the time in test environment.
Dependency injection is a broad technique and can be implemented differently. The primary patterns are:
Let’s review each of them.
I’ll use the word “factory” to mean both abstract factory and factory method patterns. Conceptually, both these patterns provide a way to encapsulating the instantiation and construction logic, hence can be generalized.
Factories usually act as injectors and wire together clients with their dependencies. The goal of factories is to decouple dependencies from their clients.
protocol Client {}
enum ClientFactory {
static func make() -> Client {
return ClientImplementation(dependency: DependencyImplementation())
}
}
class ClientImplementation: Client {
init(dependency: Dependency) {}
}
protocol Dependency { }
struct DependencyImplementation: Dependency {}
A container takes over some sort of abstractions within its bounds. It serves a wide range of functions:
The core difference from factory is that dependency injection container typically holds a link to created objects, hence the name “container”. The container is especially useful when you need to manage lots of client objects with many dependencies. Factories usually just “forget” about the instantiated objects.
Let’s see how a container can be used to assemble a VIPER module:
final class Assembly {
private let view = View()
private let presenter = Presenter()
private let interactor = Interactor()
private let router = Router()
var input: ModuleInput {
return presenter
}
weak var output: ModuleOutput? {
didSet {
presenter.output = output
}
}
init() {
view.output = presenter
interactor.output = presenter
router.output = presenter
presenter.view = view
presenter.interactor = interactor
presenter.router = router
}
}
class View {
weak var output: ViewOutput!
}
class Presenter {
weak var view: ViewInput!
weak var interactor: InteractorInput!
weak var router: RouterInput!
weak var output: ModuleOutput!
}
class Interactor {
weak var output: InteractorOutput!
}
class Router {
weak var output: RouterOutput!
}
// Declaration and conformance to input / output protocols is omitted for brevity
Assembly is a dependency injection container which instantiates, wires together and manages life cycle of VIPER module components. The container exposes module input and output ports, enforcing encapsulation.
Service Locator is controversial pattern. The idea behind is that instead of instantiating dependencies directly, we must use special locator object, responsible for looking up each dependency (i.e. service, hence the name of the pattern). Locator provides a way to register dependencies and manages their life cycles. It does not instantiate the dependencies.
Service Locator has two common implementations:
The former approach violates dependency injection, since DI is an alternative to static and global access. I suggest to follow the second strategy, which is implemented next:
protocol Locator {
func resolve<T>() -> T?
}
final class LocatorImpl: Locator {
private var services: [ObjectIdentifier: Any] = [:]
func register<T>(_ service: T) {
services[key(for: T.self)] = service
}
func resolve<T>() -> T? {
return services[key(for: T.self)] as? T
}
private func key<T>(for type: T.Type) -> ObjectIdentifier {
return ObjectIdentifier(T.self)
}
}
class Client {
private let locator: Locator
init(locator: Locator) {
self.locator = locator
}
func doSomething() {
guard let service: Service = locator.resolve() else { return }
// do something with service
}
}
class Service {}
Dependency injection is a powerful technique, which helps to design clean and maintainable applications. It allows to separate the creation of objects from their usage and reduces coupling between components.
Factory, Service Locator and Dependency Injection Container patterns describe different solutions to how dependency can be injected into a client. Our implementations outline their appliance in Swift.