1. Overview
There are many principles we can follow to design good software. Regarding module dependencies, one of the most important principles we can look at is Dependency Inversion Principle (DIP).
In this tutorial, we’ll look into the Dependency Inversion Principle with examples and why we should use it.
2. D of SOLID
SOLID principles are a collection of best practices for Object Oriented Programming. We refer to Robert C. Martin (one of the founders of the Agile Manifesto) as its inventor. They are explained in many articles and books, such as Agile Software Development, Principles, Patterns, and Practices.
These principles are:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Each one of them can stand on its own. However, as we’ll see, they have common points and relationships. For example, the DIP has a connection with the OCP and Liskov Substitution Principle. Thus, we should also be familiar with the other principles first.
3. What Is the Dependency Inversion Principle
Before diving into an example, let’s define what DIP means.
Dependency Inversion is the strategy of depending upon interfaces or abstract functions and classes rather than upon concrete functions and classes.
Simply put, when components of our system have dependencies, we don’t want directly inject a component’s dependency into another. Instead, we should use a level of abstraction between them.
Let’s imagine we are building a model of a car. The car has different parts like a steering wheel, brake peddles, engine, etc.
We build the model. Now we are testing a prototype. We find out that whenever we change or update the engine, also the steering wheel needs a fix. What is wrong with this model?
3.1. Abstractions Should Not Depend on Details
A good rule to apply is that higher-level components should not depend on a lower level.
The engine changes, and so does the steering wheel. This means an issue with a low-level part propagates to a higher level. This is not what we want. Whether our engine has changed or not, we should be able to drive a car the same way. We expect the usage detail of a component not to impact a higher-level policy.
3.2. Details Should Depend Upon Abstractions
Another good rule is that components should work in isolation.
If we change perspective, we can say that our engine should not depend on the main functionalities of the car. Lower-level components should depend on abstraction whenever possible. If we don’t follow this, the chance of creating tightly coupled modules is very high. Bugs will be hard to fix. Furthermore, extending our module will probably be impossible over time. Thus, we’ll also break the OCP, with no places in our modules we can bend or extend for new features.
3.3. Depends on Abstraction
So how do we get our components to be independent?
DIP tells us that every dependency in the design should target an interface or an abstract class. Furthermore, a dependency should not target a concrete class.
From a software build perspective, we’ll only need to recompile a dependency without affecting the other components when we change a dependency.
We can argue that although we inject an interface, we still do not eliminate a dependency. However, by applying DIP, our modules are loosely coupled and more maintainable.
4. Dependency Inversion Principle Example
Let’s wrap up the previous section with an example. Let’s say we have ClassA and ClassB. Suppose ClassA depends on ClassB. At runtime, an instance of ClassB will be created or injected into ClassA. The flow of control (or the order in which a program is executed) goes from class A to class B:
Let’s write this as some code:
class ClassB {
// fields, constructor and methods
}
class ClassA {
ClassB objectB;
ClassA(ClassB objectB) {
this.objectB = objectB;
}
// invoke classB methods
}
Now, what DIP is telling us is to invert the dependency. We can see how this applies to our diagram:
The flow of control will still follow the same path. However, now both our objects will depend on the abstraction level of the interface. Thus, ClassB inverts its dependency on ClassA. We can also create a class diagram to show how both classes now depended on abstraction:
Likewise, we can see this as code:
interface InterfaceB {
method()
}
class ClassB implements InterfaceB {
// fields, constructor and methods
}
class ObjectA {
InterfaceB objectB;
ObjectA(InterfaceB objectB) {
this.objectB = objectB;
}
...
}
5. Why Is the Dependency Inversion Principle Important
When we write code for applications, we might split our logic into multiple modules. Nonetheless, this will result in a code with dependencies. One motivation behind DIP is to prevent us from depending upon modules that often change. Concrete classes change frequently, while abstractions and interfaces change much less. For example, operations like bug fixing, code recompiling, or merging different branches will be much easier.
However, there is more to it. DIP is key to achieving loosely coupled and maintainable systems alongside concepts such as Polymorphism or Dependency Injection.
Let’s look at the importance of DIP by observing cases where it is sometimes misplaced with other concepts.
5.1. Is It Only Polymorphism?
After learning about the DIP principle, we’ll apply interfaces or abstractions to manage our modules’ dependencies. For instance, we’ll inject interfaces as a dependency in our modules. Furthermore, we can inject multiple implementations of the same interface in our ClassA. For example, let’s say now we have ClassB1 and ClassB2 extending the InterfaceB:
However, isn’t that just polymorphism?
Polymorphism indeed plays a part in the principle. However, it is not just the principle itself. This is where the concept of dependency inversion comes in. Polymorphism is in use to achieve the inversion.
Notice that we are following the Liskov Substitution Principle. This way, we can replace the ClassB with other implementations of the same interface without any break.
5.2. Is It Only Dependency Injection?
Now that we understand DIP, we’ll have a more sensible idea of injecting interfaces into other components. Therefore, we’ll inject interface implementations as part of a class constructor. For example, this is relevant in testing where we’ll provide fake implementations of our dependencies as interfaces mock.
However, isn’t that only Dependency Injection? Again, it is part of the principle.
Suppose we have some tightly coupled classes. How do we get to more manageable and loosely coupled classes?
5.3. Dependency Injection and IoC
We learned from the DIP that we could inject implementations of our interface:
That looks much better. Dependency Injection is just a vehicle to achieve the inversion. However, we can’t create instances of interfaces, so we must always depend upon concrete classes. Using a Factory Design Pattern will help us to overcome this problem.
Most likely, when it comes to the development of applications, we’ll use a Dependency Injection framework such as Spring for Java. Then, we’ll achieve one more inversion. However, this time, it’ll be about inverting the control of our concrete object creation. This is called IoC (Inversion of Control). Simply put, we’ll delegate the object creations to our IoC container.
5.4. The Context of Modules
DIP will help us to manage module dependencies. Modules are independent groups of related functionality. We can call a module also a package.
The main point of DIP is to help our modules to be truly independent. It helps to make it easier to develop and deploy an application independently.
This is relevant for statically typed languages where dependencies are known at runtime. Recompiling the whole application would take longer.
Also, with dynamically typed languages, it might be as simple as updating source code files.
Let’s give a context to the previous example using modules:
Why is the interface in the ClassA module? It needs to be done to achieve module independence. If we were to put in any other module, it would make the high-level module dependent on other modules. The interface is an abstraction through which the ClassA module can interact with the outside world.
6. Benefit of Using the Dependency Inversion Principle
Let’s summarize how DIP can help us to get loosely coupled classes:
With DIP
Without DIP
Easy to develop different components with TDD
Hard to test due to class dependencies
Easy to extend our components and apply OCP
Hard to extend as classes are tightly coupled
Easy to independently deploy parts of the system
Need to recompile all the software for a small fix
Easy to merge branches of work as changes are isolated
Hard to merge branches as code has dependencies
So, shall we always use the DIP? As much as is feasible, the principle should be followed. However, we shouldn’t create an interface for a class every time. Nonetheless, DIP is one principle we should adopt and deeply understand if we want to manage package dependencies.
7. Conclusion
In this article, we saw why the Dependency Inversion Principle is important and why we should adopt it. We have also seen a simple example where we invert the dependency of two classes using an interface. We have also explained the benefits of DIP, why we should adopt and how it relates to the other SOLID principles. DIP is relevant when working with multiple packages or for classes that change often.