Dependency Injection in Swift
Dependency injection is a scary term for a very simple idea. Instead of creating the dependency internally
an object can receive it from the outside.
This seemingly small and insignificant step opens a lot of possibilities.
The objects instantly become fully testable. A test double instead of a real service can be passed to the initializer to provide the expected responses or spy on what’s called on the Service.
Testing becomes possible without any frameworks, which also makes it fully compatible with Swift. No runtime magic is required to replace RealService with a test double during testing. A ServiceSpy object implementing the Service protocol can be created with normal development tools.
Developers can write classes that use each other. Only the interface needs to be negotiated beforehand, and the development can continue in isolation, using fake objects simulating the future real ones.
The separation of concerns improves. The creation of dependencies is separated from the client’s behavior. The client can work with everything that supports the interface it expects. This makes the whole system more loosely coupled.
When a client takes an object implementing a protocol instead of a concrete class, this isolates the client from the impact of changes in the dependency implementation.
However, it is still applicable when concrete objects are taken as services. Although this undermines the advantages of the dependency inversion principle and makes testing harder, many benefits still apply.
There are three common types of dependency injection: setter-, interface-, and constructor-based. From those three, the constructor-based is the preferable one. In the iOS world, it could be called the initializer-based injection. It’s when the dependency is passed to the client in the initializer and doesn’t change during the whole client’s life.
The initializer-based injection is preferable to others because it prevents the creation of not fully configured objects and encourages the immutability.
Yet the biggest advantage of this type could be the fact that it makes the violation of the single responsibility principle extremely obvious. If an object takes all its dependencies in the initializer and the initializer has more than three parameters, this is a hint that the object might be doing more than one thing and the refactoring is needed.
The code below shows how several objects can be composed together to provide a preferences logging feature. Each object is responsible for one thing only and doesn’t lie about its dependencies, clearly stating them in the initializer. It is impossible to mistakenly create an object that is not fully configured.
The example also shows how the business logic written in Swift can be tested without third-party mocking frameworks.
Although the dependency injection in Swift is no different from the dependency injection in other languages, it is often underrated in iOS and OS X development community. It provides a number of benefits that make the code more reusable, testable, and maintainable. These benefits can often be achieved unconsciously, simply by using the initializer-based dependency injection for all objects, because it makes the single responsibility principle violations more obvious.