V. Dependency Inversion Principle in Swift
To finish with our series of solid principles we will talk about the last principle the Dependency Inversion Principle which tells us that:
- High-level components shouldn’t depend on Low-level components, both must depend on abstractions (protocols).
- Abstractions shouldn’t depend on details. The details (concrete implementations) must depend on abstractions.
High-level components are those who manage the business logic in our app (how to present and process data, handle interactions with the UI..) that doesn’t have references to low-level implementation details.
Low-level components are conscious about delivery mechanism (CoreData, Realm, Alamofire, Firebase..).
Why do we do it, and what benefits do we get?
Because it allows us to have a less coupling between the different modules, greater facility to make new changes and test our code easily otherwise we won’t be able to test or develop our code separately.
When the high-level component communicate with low-level implementation details, it does so through the protocols, and this protocols are implemented by the low-level component depending on the actions performed from the high-level. This gives us great flexibility since through these abstractions each module doesn’t need to know how the other is implemented.
How do we get this? using the design pattern known as Dependency Injection. This design pattern is responsible for providing objects to a class instead of being created by the class itself, thus extracting the responsibility of creating an instance. so these objects are injected into the class through a property, method or initializer.
Continuing with the previous example we see that we are already complying with this principle. We have our property that depends on an abstraction through which we will communicate the high-level component with the low-level one.
To have a high-level module, it shouldn’t depend on implementation details either, as in this case ‘InfoListViewController’ that depends on UIKit, so in this case we are going to create a ViewModel that takes care of the business logic.
We create a variable of our viewmodel to which we inject the service instance through the initializer.
The view model will be in charge of communicating with the low-level component through the implementation of the abstraction and when the response is received we fire the notification through a block called refreshData that reloads the InfoListViewController table using [weak self] to avoid retain cycles
As we mentioned earlier, this makes it easier for us to mock and test our code, let’s see it with an example.
We create a class called MockDatabaseService to test the number of posts returned in this case 3. Here we are not interested in knowing the logic behind the low-level layer but rather focus on the business logic of our ViewModel. What we do is that this mock class implements the abstraction ‘DatabaseServiceProtocol’ in this way we can inject any instance that conforms to this contract (Liskov Substitution principle) and through the fetchPosts method we control the returned data to verify that the number of posts is correct.
In summary, we must have communication between the high-level and low-level components through abstractions, being the high-level component agnostic to the implementation and the low-level component in charge of implementing the abstractions requirements.
I hope it has been helpful and that you can apply it as much as possible to build higher quality software.
See you in the next post. Happy coding! 👨💻