Dependency Inversion Principle states that:
- High level modules should not depend upon low level modules. Both should depend upon abstractions.
- Abstractions should not depend upon details. Details should depend upon abstractions.
Achieving this will increase the reusability of the system modules by applying loose coupling to any dependencies among them. This will also make the system flexible enough to welcome and accept changes without harmful redesigning or restructuring efforts.
Note: high level modules are the ones that contain the system complex business logic and flows, and the low level modules are the ones that contain the system low level and basic operations like hardware access, network protocols...etc.
Let's consider the following class diagram that violates the Dependency Inversion Principle:
From the first look, we can notice that this system only reads characters from the keyboard and writes them to the printer, and nothing more. We also notice that the CopyManager (high level class) is so tightly coupled with the KeyboardReader and PrinterWriter (low level classes). Now let's consider we want to direct the output to a file or to the screen, or support to read the input characters from the screen or from a file; definitely the CopyManager will change to support these updates, and imagine that the CopyManager class contains complex business logic and flows and is hard to be tested. For sure it won't be a good practice to modify a core class like that with every single update or new added system feature.
Applying the Dependency Inversion Principle will resolve that matter, and will make the system more flexible, extendable, modifiable and testable.
A good practice to apply the Dependency Inversion Principle correctly is to revert the modules dependency while we think and design. That can be done by first defining the high level modules that contain the complex business logic that we wouldn't want to change, and then define an abstraction layer based on the high level modules needs, and make the high level modules depend on that abstraction layer. Also, to make sure we are applying the Dependency Inversion Principle right; we should seal off both the high level modules and their abstraction layer together in the same package (library), and the low level module in different package(s) (libraries); and that to guarantee total dependency isolation between the high level modules and the low level modules. Now we will not worry about the actual input sources from which we read the characters or the actual output sources to which we write them. See the diagram below that fulfills the Dependency Inversion Principle: