Dependency Injection is an essential piece of knowledge for an iOS Developer nowadays. Therefore, it is very important to know how to implement it in Swift.
In this post, I won't explain deeply what it is or when to use it, but rather leave some material about it and provide some examples.
What is Dependency Injection?
- Inversion of Control Containers and the Dependency Injection pattern
- DIP in the Wild
What are the advantages of using it?
It is essential when you are aiming for a testable code. Instead of using Singletons or making the objects create their own dependencies, you receive them from outside using a dependency injection technique. This makes testing a lot easier since you can create Test Doubles to control inputs and verify outputs of your code.
- Reduced coupling.
- Flexibility, making your application more configurable. This means that we can have many concrete implementations, but the interface won't change.
- Code reusability.
- Better testability.
- Makes the code easier to maintain in the long run.
In this article, I will present examples, succinct explanation, some tests, and discussion of the types of injection below:
With this technique, you will provide the object with everything it needs on its initialization.
Let's assume you are creating an ImagesService that uses Cache and a Network layer. With the initializer-based technique, it could be something like this:
What about the tests? How this could be used on them?
Notice that the usage of default parameters on the init method can simplify ImagesService in the application, while also making it simple to accept test doubles injection.
It consists of exposing your dependency with a public or internal access modifier to enable it to be assigned from outside. Which can be an option anytime you don’t have control over the initialization of the subject needing the dependency.
Note: This could come in handy when you have a legacy application and is starting to separate responsibilities on new components or dependencies.
Consider that you have some legacy code that does not use injection techniques, where everything happens on the controller and uses storyboards. In order to make its logic testable, you could use the technique explained above as shown below:
How could we test it?
Consists on using a parameter to inject your dependencies.
This turns out to be very useful also when dealing with legacy code since you can simply inject the dependency needed as a parameter and use a default value. Using this approach is many times less expensive than modifying the whole class to enable initializer or property-based injection.
Consider that we have another implementation of the ImageService, from the first example, but now implemented with static methods like below:
Since this static approach doesn't have initializers, it could be very painful for us to change it's implementation all over the code using initializer or property-based injection… Taking that into account, a possible approach could be extracting interfaces for Cache and Network, as we did on the initializer-based example, passing the dependencies as a parameter with a default value.
The code would be something like:
This would not change the ImageService usage along with the legacy code but will enable us to take control over Cache and Network for our tests.
The tests will end up being something like the example below:
Knowing different dependency injection techniques will help you decide which one, or which combination of them is more suitable to your problem in order to create more testable, reusable, and maintainable code.