SOLID Principles in Python - Part 3

·

3 min read

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!