Krzysztof Hrynczenko's Dev Diary

My little place where I write about things that interest me.


SOLID #5: The dependency inversion principle

Posted on January 13, 2021

The dependency inversion principle

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.

In essence, it is all about keeping our code modular, properly separating interfaces from implementations is vital to maintain modularity.

The entourage anti-pattern

There is an anti-pattern which is known as the Entourage anti-pattern which is a problem where supposedly importing one code element to the namespace brings also the neighboring elements (the entourage).

Let's look at some example.

# parser.py

import json

class JSONParser(ABC):

    @abc.abstractmethod
    def parse(json: str) -> dict:
        pass

class DefaultParser(JSONParser):
    def parse(json: str) -> dict:
        json.loads(json)

# user.py

import parser

class User:
    def __init__(json_parser: JSONParser):
        # ...

    def read(json: str):
        # ...

If you look carefully you will notice that in the user module we implicitly now made another dependency, the dependency on the json package. This is not desirable since what we really use is just the interface and the interface alone does not need the json package to be present.

I know, I know, if the parser module lies in the same package like ours, that is not much of a problem but if the interface lies in a separate package then when we add it we will be forced to download the json dependency anyway.

As a general rule, implementations should be put into separate crates (rust), assemblies (C#), packages (python).

The stairway pattern

The solution to the aforementioned problem is to put interfaces and their corresponding implementations in different packages. Something like in the example below.

# parser.py

class JSONParser(ABC):

    @abc.abstractmethod
    def parse(json: str) -> dict:
        pass

# parser_impl.py

import json

from parser import JSONParser

class DefaultParser(JSONParser):
    def parse(json: str) -> dict:
        json.loads(json)

# user.py

from parser import JSONParser

class User:
    def __init__(json_parser: JSONParser):
        # ...

    def read(json: str):
        # ...

Now the user module does not implicitly depend on the json package. If you would try to represent this way of separation of interfaces and implementations using a UML you would see something similar to a stairway, hence the name. This way of organizing your interfaces/implementations make it way easier to incorporate your code for your clients.

There is much more to the dependency inversion but I will stop right here because I think that this is the essence.

Special notes

I know that I rushed over this and the last principle. I am really time constrained these days and writing posts like the one I did on the single responsibility principle takes me a couple of hours at least.

If you would like to know more I highly recommend the "Adaptive Code" by Gary Mclean Hall. All my understanding and some of the examples I presented in this blog series are based on this book. book. In my opinion it is THE book on the SOLID principles.