Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency Inversion in Python

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")
like image 549
Bob Reynolds Avatar asked Apr 22 '20 06:04

Bob Reynolds


People also ask

What is dependency inversion in Python?

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.

What is the meaning of dependency inversion?

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.

What is dependency inversion vs dependency injection?

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.


Video Answer


2 Answers

The principle

Robert C. Martin’s definition of the Dependency Inversion Principle consists of two parts:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. 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():
        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)
like image 143
CRISPR Avatar answered Oct 06 '22 12:10

CRISPR


# 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())
like image 35
Nikos M. Avatar answered Oct 06 '22 12:10

Nikos M.