Dependency Inversion Principle 101
Module levels, abstractions, and details in just 5 minutes.
The Dependency Inversion Principle or DIP is one of the five SOLID design principles.
This principle announces:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
To understand this principle, we should first understand what does high-level modules, low-level modules, details, and abstractions stand for.
I strongly recommend reading the Clean Architecture book, written by Robert C. Martin — Author of Clean Code and The Clean Coder, and one of the main references of the SOLID design principles — but I will try to explain what do these concepts mean and how to correctly apply them via DIP.
High-level modules and Low-level modules
The module’s level is determined by its distance from the I/O.
For example, a REST Client module (External interfaces) is a low-level module, and a Commercial Policies Calculation module is a high-level module.
The DIP says that a high-level module shouldn't depend on a low-level module, so the Commercial Policies module shouldn’t depend on a Rest Client module. But… what if the high-level module needs data from an external service to work as expected? We will answer right away, but before, what are the abstractions and details?
Abstractions and Details
Abstraction is an abstract class or an interface, and detail is everything that isn’t business logic: Data persistence, external services, frameworks, UI, etc.
On one side, the “high-level” code (business logic or high-level modules) should be as agnostic as possible from the details and know nothing but abstractions. It is often said that “program against interfaces” is a good practice.
- The level of a module depends on its distance from the I/O.
- Modules that contain business logic are the highest level modules.
- When we say abstractions, usually we refer to interfaces or abstract classes.
- When we talk about details, is everything that isn’t business logic.
Dependency Inversion Principle
A few paragraphs ago I asked what happens if a high-level module depends on a low-level module, as a Rest Client module. Let’s see what happens if the Commercial Policies module needs data from an external service to work as expected.
Let’s suppose that the required data is logged user info. We could think of something like this:
This case is violating the DIP for, at least, two reasons:
- UserRestClient isn’t an abstraction but a concrete class.
- A high-level module (commercial policies) depends on a low-level module (external interfaces)
So what is the problem?
We have a high-level module depending on a low-level module. We can see this by observing where the arrow is heading.
The dependency is going from CommercialPoliciesService to the UserRestClient. This works just fine, but we are violating the dependency inversion principle, and this address some issues:
- Should the commercial policies service know that the logged user info comes from an external interface (REST Client)?
- What happens if, in the future, the REST API gets deprecated and now we should use a SOAP service?
If the REST Client is a detail (it doesn’t have any business logic), coupling a high-level module to the detail could be considered as a design error.
DIP to the rescue!
To change the heading of the arrow wouldn’t make sense because having the Rest client depending on the commercial policies would be conceptually wrong. But, at the same time, having the arrow going from CommercialPoliciesService to UserRestClient is wrong as well: as we already said, a high-level module would depend on a low-level module. How do we solve this?
Well, we can solve this matter by simply adding an abstraction between the high-level module and the low-level module.
Notice that CommercialPoliciesService does no longer know anything about a REST Client (or any other detail), but it knows that there is an interface UserDataProvider that, given a user, it returns the info about it.
Also, notice the heading of the arrows. The highest-level module depends now on abstraction, not on detail.
Also, the arrow from the REST Client is now inverted (see what we did there?). It goes from the low-level module to the high-level module.
Adding an abstraction between two modules allows us to invert the dependencies.
What do we get from this?
- To keep the dependencies going to the right place: High-level modules don’t depend on low-level modules.
- The business logic is no longer coupled to the implementation details.
- Our design is more flexible: If the user information is no longer available via REST but SOAP, it is enough to implement the UserDataProvider abstraction and any other change is transparent.
- We have now a more clean architecture, where all the dependencies go from low to high-level modules, and there aren’t cyclic dependencies.
Here is another article about why and when we should use interfaces (abstractions):
- Understanding the inversion dependencies is easier if we think about where are the arrows heading and how it changes by adding an abstraction.
- To have a better understanding of the DIP, we must first understand what does each concept stands for (abstraction, detail, high-level module, low-level module).
- Programming against interfaces is not only a matter of polymorphism and reusable code, but it also affects the system design and architecture.
- By decoupling the code of a concrete class, adding an interface in the middle, we accomplished the dependency inversion between modules and we won a cleaner architecture and flexibility for future changes.
- This technique for inverting the heading of the dependencies is easy to program, it implies only to program against an abstraction instead of a concrete class (even if it is only 1 concrete implementation so far), and it also gives us a very powerful tool to fix cyclic dependencies.
Hope you have enjoyed the reading, and please let me know any thoughts, ideas, or constructive feedback in the comments!