We have now reached the grande finale of the blog series on SOLID, the big D, The Dependency-Inversion Principle. Let’s jump right into it.
The Dependency Inversion Principle (DIP) states:
A: High-level modules should not depend on low-level modules. Both should depend on abstractions.
B: Abstractions should not depend upon details. Details should depend upon abstractions.
This needs some explaining. More traditional software development methods tend to create software structures in which high-level modules depends on low-level modules. DIP says these dependencies should be exchanged with abstractions. I’ll show you an example on how this can be done in the ”How to implement” section.
We must also define ”high-level” and ”low-level” in order to understand this properly. I like to define high-level modules as close to the Domain/Business logic and low-level as those close to the input and output of the program. A typical high-level module contains the domain models while a typical low-level module contains code that deals with I/O (for example reading user input commands or persisting data to a database).
Code that does not adhere to DIP can be hard to change and re-use. In other words it smells of Rigidity and Immobility. The smells comes from the direct dependencies, making it hard to change lower-level modules without making large changes to the high-level modules.
Assume as an example that you have a high-level class, Product, that have a direct dependency to a low-level class, SqlDb. Now, changes to the low-level class will force the high-level class to be changed. And it may also be hard to re-use the high-level class due to this direct dependency.
How to implement
In order to invert the dependencies a set of service interfaces can be added, at the same level as the current module. These interfaces should then be implemented by the lower level module. See the diagrams.
In the traditional layering style there are direct dependencies from higher-level classes to lower level classes. This means that the top level classes will be dependent on changes on the lowest level. A change to the lowest level may propagate all the way up to the top forcing a complete recompile, re-test and re-deploy of all modules in the system.
If we add an abstract service interface at all layers that have dependencies to lower layers we effectively break this dependency chain. The high-level, mid-level, and low-level modules can be put into different assemblies (projects) so that a change in one layer does not affect any other layers as long as the interface isn’t changed.
Another aspect that is different from the traditional way of putting code into layers is that the abstract interfaces are grouped with the clients, and not together with their implementations. The interfaces should be designed from the clients needs and not the other way around. Changes to the interfaces should be driven from the clients, i.e. inverted compared to traditional layering.
This post ends the series on the SOLID principles. I started with an introduction to SOLID where I explained why you should care to learn what SOLID is and also introduced some code smells that is common in large software projects. I then went through the five different SOLID principles, The Single-Responsibility Principle, The Open/Closed Principle, The Liskov Substitution Principle, The Interface Segregation Principle, and The Dependency-Inversion Principle and explained what they say, which code smells that may appear if not adhered to, and how to implement them.
I must remind you to take it easy and not add abstractions, apply patterns, and introduce these principles until you detect the smells. Doing that will add unnecessary complexity to your code, making it harder to work with and understand.
However, when you do introduce the principles, use all of them on the smelly part of the code. Now stop reading and write some code!