The Dependency Inversion Principle (DIP)

Alt text

  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.

- Robert C Martin

The core takeaway from this definition is:

The DIP splits the dependency between the high-level and low-level modules by introducing an abstraction between them.

Alt text

Bad Practice

class Developer(object):
    def __init__(self):
        print ("developer added")
	
class Designer(object):
    def __init__(self):
        print ("designer added")
	
class Manager(object):
    def __init__(self):
        self.developers=[]
        self.designers=[]
        self.testers=[]

    def addDeveloper(self,dev : Developer):
        self.developers.append(dev)
        
    def addDesigners(self,design : Designer):
        self.designers.append(design)
        	
if __name__ == "__main__":
    a=Manager()
    a.addDeveloper(Developer())
    a.addDesigners(Designer())

Bad Practise, because Manager depends on definitions of other “low-level” objects.

Good Practice

from abc import ABC,abstractmethod

class Employee(ABC):   # Could be named 'IWorkable'
    @abstractmethod
    def work(self):
        pass
	
class Manager(Employee):
    def __init__(self):
        self.employees=[]
    def addEmployee(self,e : Employee):
        self.employees.append(e)
    def work(self):
        # delegate tasks to employees:
        for e in self.employees:
            e.work()

class Developer(Employee):
    def __init__(self):
        print ("developer added")
    def work(self):
        print ("turning coffee into code")
        
class Designer(Employee):
    def __init__(self):
        print ("designer added")
    def work(self):
        print ("turning lines to wireframes")
          
if __name__ == "__main__":
    a=Manager()
    a.addEmployee(Developer())
    a.addEmployee(Designer())
    a.work()

Good Practise: Manager and employees depend only on abstraction Employee

There are many different approaches to use the DIP in different programming languages.

However, an example for a straightforward, standard approach is given below. It is C++ but could be easily implemented other OO languages as well.

#include<iostream>
#include<memory>

struct IMovable { // Interface for movements of objects
    virtual void move() = 0;
};
class Animal {
    std::unique_ptr<IMovable> move_;
    std::string name;
public:
    Animal(std::string name_, std::unique_ptr<IMovable> move) // lifecycle of move - Object is now bound to Animal
        : move_(std::move(move)), name(name_) {}
    std::string get_name() const { return name; }

    void move() {  // delegate to 'IMovable'
        return move_->move(); 
    }
};
struct swim : public IMovable {
    void move() override { std::cout << "swim"; }
};
struct gallop : public IMovable {
    void move() override { std::cout << "gallop"; }
};

int main()
{
    auto move_animal = [](Animal& animal) {
        std::cout << "Move Animal " << animal.get_name() << " : ";
        animal.move();
        std::cout << "\n";
    };

    move_animal(Animal("Marty", std::make_unique<gallop>()));
    move_animal(Animal("Dory", std::make_unique<swim>()));

    return 0;
}

Good Practice @ LE

A use case for dependency inversion at LE is the usage of SMILE Models in OPTISIM or UNISIM. SMILE Models inject a new functionality (i.e. a unit model) in these simulators.

Lets assume there is a SMILE Model (DLL) Membrane.

As it is a SMILE model, the model evaluation (residual or outgoing values) are made available by the SMILE_MODEL_DLL_API. Physical Properties are accessed using the LINDE_OPTISIM_PROPERTIES_API.

Dependencies in OPTISIM

digraph G{
    rankdir = BT;
    subgraph {
        OPTISIM -> SMILE_MODEL_DLL_API;
        Membrane -> SMILE_MODEL_DLL_API;
        OPTISIM -> LINDE_OPTISIM_PROPERTIES_API;
        Membrane -> LINDE_OPTISIM_PROPERTIES_API;
        {rank = same; SMILE_MODEL_DLL_API;}
        {rank = same; OPTISIM; Membrane; }
    }
}

Dependencies in UNISIM

The same model (DLL) Membrane can be used in UNISIM. Then, the dependencies are

digraph G{
    rankdir = BT;
    UNISIM -> "unisimdesign.hh"
    UNISIM_SMILE_Extn -> "unisimdesign.hh";
    UNISIM_SMILE_Extn -> SMILE_MODEL_DLL_API;
    Membrane -> SMILE_MODEL_DLL_API;
    "LINDE_OPTISIM_PROPERTIES_V1.dll" -> LINDE_OPTISIM_PROPERTIES_API;
    Membrane -> LINDE_OPTISIM_PROPERTIES_API;
}

Bad Practice @ LE

It would be advantageous if the physical property calculation used by OPTISIM would be actually losely coupled - that would facilitate replacing it at some point considerably.