Dependency inversion principle

This principle mainly state that, a module should "Depend upon abstractions, [not] concretions.", and more precisely:

  • 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.
A module could be a function, a class, a file or any piece of code. In practise, it means that most of the time, you should not create object within module (i.e. class function), but you should make those module depends on interface. Like demonstrated in the example below, you should avoid using if statement for object creation and replace them with dependencies on interfaces.

Code example

Bad implementation
  
    from dataclasses import dataclass
    from abc import abstractmethod
    from typing import List
    
    
    # Bad implementation
    @dataclass
    class Dog():
        size: str
    
        @abstractmethod
        def bark(self):
            pass
    
    
    @dataclass
    class BigDog(Dog):
        size: str = "big"
    
        @abstractmethod
        def bark(self):
            print("WOF WOF")
    
    
    @dataclass
    class SmallDog(Dog):
        size: str = "small"
    
        @abstractmethod
        def bark(self):
            print("wof wof")
    
    
    def barks(size_of_dog: str):
        if size_of_dog == "small":
            dog = SmallDog()
            dog.bark()
        elif size_of_dog == "big":
            dog = BigDog()
            dog.bark()
        else:
            raise NotImplementedError
    
    
    barks("big")
  
The example above is bad because the object is created within the function and you add an useless if/else statement that will be harder to maintain (more difficult to handle bug, more difficult to add new object, ...). It is way more simple / maintenable to make your function depends on an external object and to make this object not a concretion but an abstraction / an interface.
Good implementation
  

    # Good implementation


    @dataclass
    class Dog():
        size: str
    
        @abstractmethod
        def bark(self):
            pass
    
    
    @dataclass
    class BigDog(Dog):
        size: str = "big"
    
        @abstractmethod
        def bark(self):
            print("WOF WOF")
    
    
    @dataclass
    class SmallDog(Dog):
        size: str = "small"
    
        @abstractmethod
        def bark(self):
            print("wof wof")
    
    
    def barks(dog: Dog):
        dog.bark()
    
    
    barks(BigDog())

  
Now, the barks function depends ont he interface and not the object concretion. the function barks may use any Dog object and we are sur that every Dog object will have a bark method, because of the interface. Why do we call this principle the "dependency inversion principle" , because now, the dependency goes to the client. The client (i.e. the barks function) is the one that want to produce the barks sound. The client is some piece of code that is going to use bark function. The client doesn't know exactly what is going to barks(), but know that the object will have a bark() method.

References

  • https://en.wikipedia.org/wiki/Dependency_inversion_principle
  • "Clean Architecture: A Craftsman's Guide to Software Structure and Design" by Robert Martin
  • https://stackoverflow.com/questions/61358683/dependency-inversion-in-python