In this blog post, I am going to explain what is dependency inversion principle, why is it important and how to implement dependency inversion principle in java.
When i started learning about SOLID principles, the dependency inversion was the most confusing principle for me to fully understand. This article explains this principle in the easiest way possible, with multiple code examples for better understanding.
What is Dependency Inversion Principle?
The dependency inversion principle states:
- High-level modules should not depend on low-level modules (which handle implementation details). Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
For me, when i first read about this principle i thought what are high-level modules, what are low-level modules and what are abstractions?
In order to explain this better, let me take a very simple example. Let’s first look at a tightly coupled example in which Computer class is tightly coupled with a keyboard class.
1 2 3 4 5 6 7 8 |
//Keyboard class class Keyboard { public void type() { System.out.println("Typing..."); } } |
1 2 3 4 5 6 7 8 9 10 |
//Computer class class Computer { // Direct dependency private Keyboard keyboard = new Keyboard(); public void useKeyboard() { keyboard.type(); } } |
In the above example, Computer class is a high-level module which depends on the Keyboard class (low level module).
Let’s assume, tomorrow we want to replace Keyboard class with WirelessKeyboard or GamingKeyboard class. Then in that case, we have to modify the computer class, which violates open-closed principle as changes in one module affect another.
As per this principle, to make code loosely coupled, both high and low level modules should depends on abstraction. How we can introduce the abstraction which makes code loosely coupled?
We can use an interface to create that abstraction on which both Computer (high-level module) and Keyboard classes (low-level module) depends.
Dependency inversion principle video tutorial
As you can see, we introduced InputDevice interface on which both Computer and Keyboard classes depends. After this change the code becomes loosely coupled and we can easily introduce any new type of Keyboard without modifying computer class.
1 2 3 4 5 6 |
//Interface interface InputDevice { void type(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//Different keyboard classes implement InputDevice interface class Keyboard implements InputDevice { public void type() { System.out.println("Typing with Keyboard..."); } } class WirelessKeyboard implements InputDevice { public void type() { System.out.println("Typing with Wireless Keyboard..."); } } class GamingKeyboard implements InputDevice { public void type() { System.out.println("Typing with Gaming keyboard..."); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
//Computer Class class Computer { private InputDevice inputDevice; public Computer(InputDevice inputDevice) { //Dependency Injection this.inputDevice = inputDevice; } public void useInputDevice() { inputDevice.type(); } } public class Main { public static void main(String[] args) { InputDevice keyboard = new Keyboard(); Computer computer = new Computer(keyboard); computer.useInputDevice(); InputDevice wirelessKeyboard = new WirelessKeyboard(); Computer computer2 = new Computer(wirelessKeyboard); computer2.useInputDevice(); } } |
Example 2-
Similarly, In this example the ShoppingCart class has direct dependencies of multiple payment methods. So, If we introduce any new payment method, It leads to the modification of the ShoppingCart class.
To make them loosely coupled, let’s introduce an abstraction on which both ShoppingCart and different payment methods depends.
Conclusion
- Dependency inversion principle helps in writing cleaner, modular and more maintainable code.
- By coding to abstractions (interfaces), we improve maintainability and testability of a code.