Table of contents
Dependency Inversion Principle (DIP) states one single thing;
- When we modify low-level modules, it should not break the high-level modules.
In complex systems, high-level modules call lower level modules. Any change in low-level modules will break the high-level ones because the dependency is from the top down. Let's explain this with an example.
Assume that we have a module and it calls a class. So it depends on the class.
What if we invert the dependency arrow? If the class depends on the module, then we have more freedom to make changes in the class without fear of breaking things. Then, how do we achieve this? Well, we can implement a sort of interface (or an abstract class) in between.
I want you to pay attention to the dependency arrow. Normally, the Module was depending on the class, but now they are both depending on an intermediate interface. We inverted the arrow (partially) and now the dependency has changed! We can freely do changes within our class because as long as the interface is there, nothing will be broken! This is what dependency inversion is. We invert dependencies selectively to increase the rigidity of our object-oriented system.
Bad Example
If we explain this with an example of the Vehicle class which uses PetrolEngine class as an engine. Thereafter, it uses the start
method to start up the engine. Imagine that one day, you modify your PetrolEngine class and change the start
method behavior. Then you have to modify as well the main Vehicle
class. This violates DIP.
class PetrolEngine:
def start(self):
print("Petrol Engine is starting..")
class Vehicle:
def __init__(self, engine: PetrolEngine):
self.engine = engine
def engine_start(self):
self.engine.start()
def accelerate(self):
print("accelerating..")
p = PetrolEngine()
car1 = Vehicle(p)
car1.engine_start()
car1.accelerate()
Good Example
What we can do is we can implement an abstract class for PetrolEngine
class. This will set rules and a blueprint for how PetrolEngine and likewise classes. Vehicle
class will depend on this abstract class (an interface). If we re-write the classes following this new structure, we have something like;
from abc import ABC, abstractmethod
class Engine(ABC):
@abstractmethod
def start(self):
pass
class PetrolEngine(Engine):
def start(self):
print("Petrol engine is starting..")
class HybridEngine(Engine):
def start(self):
print("Hybrid engine is starting..")
class Vehicle:
def __init__(self, engine: Engine):
self.engine = engine
def engine_start(self):
self.engine.start()
def accelerate(self):
print("accelerating..")
p = PetrolEngine()
car1 = Vehicle(p)
car1.engine_start()
car1.accelerate()
h = HybridEngine()
car2 = Vehicle(h)
car2.engine_start()
car2.accelerate()
Now, Vehicle
class depends not on purely PetrolEngine
but only on an interface; Engine
. We can remove PetrolEngine class, or we can introduce new ones such as HybridEngine
or DieselEngine
. Whatever engine type you like you can re-introduce it without changing anything.
With this blog post, we covered all the SOLID principles. SOLID principles allow us to write easily extendible and maintainable code in object-oriented programming. The examples are given in Python but you can apply them in other object-oriented languages. All the code examples are uploaded to GitHub for the record.
This is the end of the SOLID principles series. Hope you enjoyed it!