The Dependency Inversion Principle (DIP)
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.
- 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.
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

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

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.