I've started to apply SOLID principles to my projects. All of them are clear for me, except dependency inversion, because in Python we have no change to define variable in type of some class inside another class (or maybe just I don't know). So I've realized Dependency Inversion principle in two forms, and want to know which of them is true, how can I correct them. Here are my codes:
d1.py
:
class IFood:
def bake(self, isTendir: bool): pass
class Production:
def __init__(self):
self.food = IFood()
def produce(self):
self.food.bake(True)
class Bread(IFood):
def bake(self, isTendir:bool):
print("Bread was baked")
d2.py
:
from abc import ABC, abstractmethod
class Food(ABC):
@abstractmethod
def bake(self, isTendir): pass
class Production():
def __init__(self):
self.bread = Bread()
def produce(self):
self.bread.bake(True)
class Bread(Food):
def bake(self, isTendir:bool):
print("Bread was baked")
The dependency inversion principle states that: 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.
The Dependency Inversion Principle (DIP) states that high level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details.
The Inversion of Control is a fundamental principle used by frameworks to invert the responsibilities of flow control in an application, while Dependency Injection is the pattern used to provide dependencies to an application's class.
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.
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.
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():
print('Smells like bread')
Now let's create some cookies!
class Cookies(Bakable):
def bake():
print('Cookie smell all over the place')
OK! now let's cook them.
cookies = Cookies()
bread = Bread()
cook(cookies)
cook(bread)
# define a common interface any food should have and implement
class IFood:
def bake(self): pass
def eat(self): pass
class Bread(IFood):
def bake(self):
print("Bread was baked")
def eat(self):
print("Bread was eaten")
class Pastry(IFood):
def bake(self):
print("Pastry was baked")
def eat(self):
print("Pastry was eaten")
class Production:
def __init__(self, food): # food now is any concrete implementation of IFood
self.food = food # this is also dependnecy injection, as it is a parameter not hardcoded
def produce(self):
self.food.bake() # uses only the common interface
def consume(self):
self.food.eat() # uses only the common interface
Use it:
ProduceBread = Production(Bread())
ProducePastry = Production(Pastry())
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With