1

I've been trying to understand dependency inversion in python. I understand the theory that everybody quotes but I've not yet seen the code example with and without inversion that would clearly demonstrate the benefits. I've found only one highly rated answer here that shows the code I pasted below but if you scroll down I also pasted same code without abstraction and it does the same thing and it is also modular. The only benefit I see is that using abstraction prevents someone making changes to the name of the method...

I am still struggling to understand the need for dependency inversion. Looking at the code below one uses dependency inversion and the other does not. They both seem to accomplish the same purpose and are equally modular... What's going on?

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): 
        self.food = food 
    def produce(self):
        self.food.bake() 

    def consume(self):
        self.food.eat()

ProduceBread = Production(Bread())
ProducePastry = Production(Pastry())

ProducePastry.consume()

vs.

class Bread():
    def bake(self):
        print("Bread was baked")
    def eat(self):
        print("Bread was eaten")

class Pastry():
    def bake(self):
        print("Pastry was baked")
    def eat(self):
        print("Pastry was eaten")

class Production:
    def __init__(self, food):
        self.food = food

    def produce(self):
        self.food.bake()

    def consume(self):
        self.food.eat()

ProduceBread = Production(Bread())
ProducePastry = Production(Pastry())

ProducePastry.consume()

I've been playing with the code trying to spot an obvious benefit of dependency inversion in python but with no good results.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
matt
  • 11
  • 3
  • 1
    Those are both examples of dependency inversion in my opinion. The former just happens to include inheritance of the objects whereas the latter relies on duck typing. – Cory Kramer Feb 02 '23 at 13:39
  • You are, in a real sense, a person who's never had a headache asking what the point of aspirin is. We can use the words, but you'll never really understand until you've *felt the pain yourself*. You'll have a thing that gets to be a giant hairy ball of mud that you realize should really be refactored into 3~4 different things but you can't do it easily because your code implicitly relies on the thing being in scope all over the place. Note that as others have said *both* of your code examples use DI. A proper non-DI example would be importing a module directly from a package or using a closure – Jared Smith Feb 02 '23 at 14:01
  • Note that DI is (I think) more commonly used to refer to dependency *injection*, which is a design pattern intended to automate or abstract away the very implementation of dependency inversion. – chepner Feb 02 '23 at 14:06

2 Answers2

4

Both examples use dependency injection. The only difference is that IFood is a quasi-abstract base class the indicates its subclasses should define bake and eat.

The alternative would be a definition of Production similar to

class Production:
    def __init__(self, food): 
        self.food = food 
    def produce(self):
        if isinstance(food, Bread):
            print("Bread was baked")
        elif isinstance(foo, Pastry):
            print("Pastry was baked")
        else:
            print(f"I don't know how to bake {self.food}")

    def consume(self):
        if isinstance(food, Bread):
            print("Bread was eaten")
        elif isinstance(foo, Pastry):
            print("Pastry was eaten")
        else:
            print(f"I don't know how to eat {self.food}")

If another food product were created, you would need to modify the definitions of Production.produce and Production.consume into ever longer if statements. With dependency inversion, the only thing you need t do is define your new Food subclass; Production itself doesn't need to change at all.


A more idiomatic version of the IFood-based solution would use the abc module. Type hints are added to more explicitly demonstrate the role of IFood in Production.

from abc import ABC, abstractmethod

class Food(ABC):
    @abstractmethod
    def bake(self):
        pass

    @abstractmethod
    def eat(self):
        pass


class Bread(Food):
    def bake(self):
        print("Bread was baked")

    def eat(self):
        print("Bread was eaten")

class Pastry(Food):
    def bake(self):
        print("Pastry was baked")

    def eat(self):
        print("Pastry was eaten")


class Production:
    def __init__(self, food: Food): 
        self.food = food 

    def produce(self):
        self.food.bake() 

    def consume(self):
        self.food.eat()

bread_producer = Production(Bread())
pastry_producer = Production(Pastry())

pastry_producer.consume()

If you wanted to add a third type of food to produce, just define a new subclass of Food.

class Bacon(Food):
     def bake(self):
         print("Bacon was fried")

     def eat(self):
         print("Bacon was devoured")

bacon_producer = Production(Bacon())
bacon_producer.produce()
bacon_producer.consume()

An example of dependency inversion that you use frequently in Python is the str type. You can pass values of many different types to str and get back a string.

>>> str(1)
'1'
>>> str(3.14159)
'3.14159'
>>> str(int)
"<class 'int'"

str itself doesn't know how to do this, i.e., the definition of str does not look like

def str(x):
    if instanceof(x, int):
        return int_to_str(x)
    elif instanceof(x, float):
        return float_to_str(x)
    elif instanceof(x, type):
        return type_to_str(x)
    elif ...

Instead, every value that you want to turn into a str provides its own function for doing so: instead of the result given x depending on str, the behavior of str depends on x: the dependency has been inverted.

def str(x):
    return x.__str__()

Now, if you want to define a class X and have str(X()) output something other than '<X object at 0x28342827>', you just define X.__str__ instead of having to modify your Python interpreter itself to change the built-in definition of str.

>>> class X:
...    def __str__(self): return "I'm an X!"
...
>>> str(X())
"I'm an X!"
chepner
  • 497,756
  • 71
  • 530
  • 681
  • So, if I have MVC model for instance and in the controller I run a method that takes input from the view entry box and saves passes it to the model method to save in the database, would it be dependency inversion, or would I have to have another abstract layer between controller and the model, and if so, what would be the purpose? – matt Feb 02 '23 at 14:04
  • It depends on how many different models, views, and controllers you have, and what kind of interactions they have. The core idea of dependency inversion is that instead of having to make a decision inside a function, that decision is moved *outside* the function and the result is passed as an argument. The `__str__` method is a good example in Python. `str` itself does not have to be modified every time you want to add the ability to convert your type `Foo` to a string; you just define `Foo.__str__`, and let `str` call it to produce the string. – chepner Feb 02 '23 at 14:11
  • Inheritance itself is a form of dependency inversion. Overriding a method lets the subclass define it own behavior, rather than the parent having to be modified to accommodate every subclass. – chepner Feb 02 '23 at 14:19
  • I see, so basically dependency inversion is the avoidance of direct modification of classes and using built ins, polymorphism and abstract classes to accomplish that...? – matt Feb 02 '23 at 14:23
  • Yes. It's not even specific to OOP: OOP just tends to overengineer the practice :) Consider a trivial example: `def foo(x): return x % 2 == 0`. It determines if the remainder mod 2 of x is 0. `foo` has a dependency: it needs to know how to compute `x % 2`. You can *invert* that dependency simply by making someone *else* provide that. It can be as simple as having `foo` take a remainder as its argument: `def foo(x): return x == 0`. Now instead of calling `foo(6)`, you call `foo(6 % 2)`. That's all dependency inversion is: moving some bit of logic from *inside* one unit to *outside* that unit. – chepner Feb 02 '23 at 14:37
  • (A lot of the cruft associated with dependency inversion, IMO, is due to the lack of support for higher-order functions in certain OOP languages. Instead of just passing a function to satisfy a dependency, the function must first be defined as a method of some class whose sole purpose is to provide a first-class value that can be passed as a function argument.) – chepner Feb 02 '23 at 14:40
  • That's a very good example and explanation. Thanks. I think the problem with most explanations found online is that they show overly complicated examples and obscure the the true nature of the problem at hand. – matt Feb 02 '23 at 16:23
  • Even this, I think, I've strayed too far from inversion to injection. There was a really good blog post I read once (involving Haskell and/or Scala) that made me realize just how simple inversion really is. You're just pushing decision making up a level. – chepner Feb 02 '23 at 16:33
  • @chepner, what if there is state and init involved, like in [here](https://stackoverflow.com/questions/76737681/how-do-i-decouple-lamp-from-button-using-the-dependency-inversion-principle) – progmatico Jul 21 '23 at 13:50
1

It is not the easiest to demonstrate DI on a small scale/toy projects. Yes of course, when your entire behaviour can fit into 20 lines, it will not do anything. The value comes, when you

A) have a larger project with multiple modules interfacing with each other, and/or B) make use of Python's Type hint system

Let's examine these:

On a larger project, DI with base classes provides a contract for what you need a class to implement. Thereby you decouple the behaviour of the implementation and the consumption. It also makes it easier to modify parts, without having to consider the consumer of the class.

Reworking your example, starting with:

class IPersistance:
    def save(self, key, data): raise NotImplementedError
    def load(self, key): raise NotImplementedError

class DiskPersistance:
    def save(self, key, data):
        print("I'm saving the data to disk")

    def load(self, key):
        print("I'm loading the data from disk")

class Service:
    def __init__(self, persitance):
        self._persistance = persistance

    def do_operation(self, some_data):
        self._persistance.save(some_key, some_data)

Now if you want to add a new type of persistance, for example a database, you know exactly what the consumer of IPersistance expects. And you can freely replace DiskPersistance in Service with DBPersistance for example.

Next, type hints:

Having the base class (interface) allows you to abstract away the question of what will this method return/expect as argument. You have had to define that ahead of time, and then understand that any implementation will have to accept and return the types as specified by the base class. And your consumer can then fully ignore any kind of implementation, and know what types it has to pass in, and what to expect as result.

Updating the above example:

class IPersistance:
    def save(self, key: int, data: str) -> None: raise NotImplementedError
    def load(self, key: int) -> str: raise NotImplementedError

class DiskPersistance:
    def save(self, key: int, data: str) -> None:
        print("I'm saving the string data to disk using the numeric id")

    def load(self, key: int) -> str:
        print("I'm loading the str data from disk")

class Service:
    def __init__(self, persitance: IPersistance):
        self._persistance = persistance

    def do_operation(self, some_data: str):
        some_key = 1
        self._persistance.save(some_key, some_data)

And as the folks in the comments have pointed out, your examples is still dependency inversion, just without the type guarantees.

tituszban
  • 4,797
  • 2
  • 19
  • 30