33

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")
djvg
  • 11,722
  • 5
  • 72
  • 103
Bob Reynolds
  • 929
  • 3
  • 8
  • 21
  • 4
    actualy both a wrong approaches for dependency injection as Product in both approaches explicitly instantiates Bread class, while this should be a transparent parameter class that simply matches a common interface. So Product in one case woiuld instantiate `Bread`, while in another it could instantiate `Pastry` **without any of them being hardcoded, but passed as parameters** – Nikos M. Apr 22 '20 at 06:40
  • @NikosM. OK. Then https://en.wikipedia.org/wiki/SOLID – Bob Reynolds Apr 22 '20 at 06:41
  • @NikosM. can you show it as code ? Please – Bob Reynolds Apr 22 '20 at 06:42
  • Ok, still same applies – Nikos M. Apr 22 '20 at 06:42
  • I was referring to *dependency injection*, but dependency inversion, is what is stated, use a common abstraction, instead of concrete implementations – Nikos M. Apr 22 '20 at 06:43
  • @NikosM. can you show it in form of code, man ? – Bob Reynolds Apr 22 '20 at 06:45

3 Answers3

69

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(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)
CRISPR
  • 891
  • 1
  • 6
  • 6
  • 1
    Would it be bad if bake took in some potential arguments? Would that have to be enforced by the interface somehow? or left up to the clients to know the api for whatever uses them to maintain the desired arguments? – eagle33322 Dec 02 '21 at 19:26
  • 7
    This is AN EXCELLENT explanation! – Ulf Aslak Dec 16 '21 at 12:37
  • @eagle33322 afaik, as an abstract class `Bakable.bake()` has no way of [enforcing function signatures](https://stackoverflow.com/a/25183884/8527838). You can though define `bakable.bake(**kwargs)`, which allows you to define `Cookie.bake(flavor)`, and make no changes to `Bread.bake()` – jryan14ify May 27 '22 at 21:51
  • In the sub-classes, the bake() method should be implemented as bake(self). – Igor Micev Jun 01 '22 at 20:44
  • Neat and easy to understand! Thank you so much :D – Varad Pimpalkhute Oct 08 '22 at 14:50
  • The example with the cook(food: str) also violates the Open Closed Principle and the solution also solves this. OCP: software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. With every new type of food the cook method has to be modified. – Thirdman Jul 15 '23 at 13:20
31
# 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 dependency 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())
user
  • 5,370
  • 8
  • 47
  • 75
Nikos M.
  • 8,033
  • 4
  • 36
  • 43
  • 1
    @BobReynolds, welcome, glad it helps. Note that there are many approaches to dependnecy inversion and injection not a single one. But all share the same common principles – Nikos M. Apr 22 '20 at 06:51
  • 7
    You can consider making `bake` and `eat` in `IFood` abstract methods and/or let them raise `NotImplementedError` if you want a little more restriction / self documenting code. – timgeb Apr 22 '20 at 07:25
  • 3
    @timgeb, right, I've already did it in my project. We are poor interfaceless guys )) – Bob Reynolds Apr 22 '20 at 08:15
  • 4
    Note that explicitely creating the IFood interface makes much more sense if you use it with type hints, like this: `def __init__(self, food: IFood)` – Michał Jabłoński Sep 03 '20 at 09:27
0

(Very) arguably, one of the great features of Python is dynamic/duck typing. As long as the object passed in implements the specific methods and members needed by the function, everything should work.

By using a static type checker and the typing library's Protocol feature, we can achieve Dependency Inversion while still enjoying the freedom of dynamic typing. Notably, using ABC and @abstractmethod doesn't check for compatible signatures on the implemented methods, and does not allow duck typing.

When using Protocol, the implemented class doesn't even need to reference or know about the Protocol/interface, allowing the use of external classes. In a codebase with multiple devs, commit hooks can verify any changes to any internal implemented class.

Using Protocol

from typing import Protocol

class Food(Protocol):
    def bake(self, isTendir: bool) -> str: ...
    

class Bread:
    def bake(self, isTendir: bool) -> int:  # Note the incompatible signature
        return 4
    

# This implementation can come from an external library
class Duck:
    def bake(self, isTendir: bool) -> str:
        return "quack"
    

def bake(food: Food) -> str:
    return food.bake(True)


def main():
    bake(Bread())  # Static typing check error: "int" is incompatible with "str"
    bake(Duck())

Further experiments with ABC

Static type checking the below snippet (using PyLance in Vscode) is happy to allow the line,

    bake(Bread())

which does not properly implement the bake method (returning int instead of str).

And the static type checker flags an error at line,

    bake(Duck())

which is perfectly valid/"safe" Python code that will run. Sometimes you may want to enforce that an object implements a specific Abstract Base Class, but I don't think that would normally be necessary for the spirit of Dependency Inversion.

from abc import ABC, abstractmethod

class Food(ABC):
    @abstractmethod
    def bake(self, isTendir: bool) -> str: ...
    

class Bread(Food):
    def bake(self, isTendir: bool) -> int:
        return 4
    

class Duck:
    def bake(self, isTendir: bool) -> str:
        return "quack"
    

def bake(food: Food) -> str:
    return food.bake(True)


def main():
    bake(Bread())
    bake(Duck())  # Static type checking error: "Duck" is incompatible with "Food"
kitizz
  • 51
  • 1
  • 6