Introducing Dependency Inversion PrinciplePosted by BrainConcert
The last principle from the S.O.L.I.D. set of principles (at least in the order of the acronyms) is Dependency Inversion Principle (hence the letter D). It relates to the way dependencies between modules can make the design of a module rigid, difficult to reuse and quite fragile on modifications.
The name might be bit confusing at this moment due to the evolution of the design practices and methodologies. Inversion compare to what? Inversion compare to the design part of Structure Analysis and Design Technique. This technique is introducing a Top-Down approach when designing a system, by breaking the higher, bigger modules to lower, smaller and more manageable modules. Which makes sense, correct? When designing it like that, we create a dependency of the higher modules to lower modules; keep in mind that the dependency is transitive, so if Module A depends on A.1 it also depends on A.1.1. Why is this bad? Is bad because usually in higher level modules resides business logic that, in case we want to re-use, it must be either taken together with all lower level modules on which it depends or needs to be modified for the new use.
At lower levels, modules tends to be very simple, containing primitive operations, hence easy to re-use. At higher levels, the complexity increases.
The principle is calling upon these dependencies and says we should inverse them. We should not block higher level modules to be re-used due to strong dependencies on lower level modules. And this can be done using abstractions.
This is also what the principle formally is stating:
A. High Level Modules Should Not Depend Upon Low Level Modules. Both Should Depend Upon Abstractions.
B. Abstractions Should Not Depend Upon Details. Details Should Depend Upon Abstractions.
Let's look at an example to better understand this. We have a Copy module that copies data from Console, while the user enters it and sends it to a Printer to print it out. We can imagine, for the sake of complexity that is also does some data validations, data cleansing before the input is send to the Printer (kind of a ETL module). This is represented in the following picture:
Copy Module is higher level module that is depending on Console and Printer modules, which are lower level modules. These two modules can be easily re-use in other applications, a module dealing with the Console or with a Printer is general enough for other purposes also. But what if I want to re-use the Copy module, and want to apply the same transformations and validations of data, but now I need to transfer data from console to a file? I cannot, because the module depends on Console and Printer module, not on Console and a File module. One way is to change the Copy module and introduce a dependency on File. But then again this violates the Open-Closed principle
What needs to be done is to make Copy module depends on abstractions rather than on concrete lower level modules. Supposer we would have this design:
By depending on Input and Output abstractions, Copy module becomes reusable. Whenever you want to use it, you need to specify concrete implementations for the two abstractions and the module will work just fine, without any change in its code, as long as the contract remains the same (by contract we refer to the abstractions).
One could make a legitimate complaint about the previous design. What if I already have a File module, but it doesn't comply with Output abstraction and still I want to use it? One easy way would be to make a derived class from File that also implements Output abstraction but that would be not very elegant and in some cases impossible (consider that Output is an abstract class, there are programming languages that do not allow multiple inheritance).
The best solution is to make an adapter. The adapter implements Output abstraction, and hold a reference to your File module. For all methods defined in the abstraction, the adapter make the translations to the methods offered by the File module and everything works just fine. Find in the image below this representation:
In this article we used the term module to be more general. But this can easily translates to classes (a module with a single class) or components that are a collection of classes which implements them. So, when we say abstraction this can mean an interface or an abstract class.
The principle of dependency inversion is at the root of many of the benefits claimed for object-oriented technology. Its proper application is necessary for the creation of reusable frameworks. It is also critically important for the construction of code that is resilient to change. And, since the abstractions and details are all isolated from each other, the code is much easier to maintain.