The principle
Robert C. Martin’s definition of the Dependency Inversion Principle
consists of two parts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Just to clarify... a module could be a function, a class, a file... a piece of code.
The mistake
Let's say you have a program that needs you to bake bread.
On a higher level, there is a point where you could call cook()
A bad way of implementing this is to make a function that cooks but also creates the bread.
def cook():
bread = Bread()
bread.bake()
cook()
This is not good...
As you can see, the cook
function depends on the Bread
.
So what happens if you want to bake cookies?
A rookie mistake is to add a string parameter like this:
def cook(food: str):
if food == "bread":
bread = Bread()
bread.bake()
if food == "cookies":
cookies = Cookies()
cookies.bake()
cook("cookies")
This is obviously wrong. Because by adding more foods you change your code and your code becomes a mess with many if statements. And it breaks almost every principle.
The solution
So you need the cook
function which is a higher level module, not to depend on lower level modules like Bread
or Cookies
So the only thing we need is something that we can bake. And we will bake it. Now the right way to do this is by implementing an interface. In Python it is not necessary, but it is a strong recommendation to keep code clean and future-proof!
If it looks like a duck, swims like a duck, and quacks like a duck,
then it probably is a duck.
They say.
Now let's invert the dependency!
from abc import ABC, abstractmethod
class Bakable(ABC):
@abstractmethod
def bake(self):
pass
def cook(bakable:Bakable):
bakable.bake()
And now the cook
function depends on the abstraction. Not on the Bread, not on the Cookies but on the abstraction. Any any any Bakable
can be baked now.
By implementing the interface we are sure that every Bakable
will have a bake()
method that does something.
But now cook
function does not need to know.
cook function will bake anything that is Bakable
.
The dependency now goes to the client. The client is the one that want to bake something. The client is some piece of code that is going to use cook
function. The client knows what is going to be baked.
Now by looking at the cook
function, the client knows that cook
function waits to receive a Bakable
and only a Bakable
.
So let's create some bread.
class Bread(Bakable):
def bake(self):
print('Smells like bread')
Now let's create some cookies!
class Cookies(Bakable):
def bake(self):
print('Cookie smell all over the place')
OK! now let's cook them.
cookies = Cookies()
bread = Bread()
cook(cookies)
cook(bread)