- Don’t Repeat Yourself (DRY) – Wissenshäppchen #1
- You Ain’t Gonna Need It (YAGNI) – Wissenshäppchen #2
- Single Responsibility Principle (SRP) – Wissenshäppchen #3
- Open Closed Principle (OCP) – Wissenshäppchen #4
- Liskov Substitution Principle (LSP) – Wissenshäppchen #5
- Interface Segregation Principle (ISP) – Wissenshäppchen #6
- Dependency Inversion Principle (DIP) – Wissenshäppchen #7
- Law of Demeter (LoD) – Wissenshäppchen #8
Im siebten Wissenshäppchen geht es um das Dependency Inversion Principle.
Podcast: Play in new window | Download (Duration: 18:03 — 8.3MB)
Abonnieren: Apple Podcasts | Spotify | RSS
Inhalt
Das DIP ist das letzte der fünf SOLID-Prinzipien.
High level modules should not depend upon low level modules. Both should depend upon abstractions.
Oder:
Abstractions should not depend upon details. Details should depend upon abstractions.
Welche Abhängigkeiten werden hier „umgedreht“? Komponenten, die andere Komponenten benötigen, erzeugen sich diese nicht selbst, sondern bekommen sie von außen hineingegeben. Braucht ein Service
beispielsweise ein Repository
um arbeiten zu können, erzeugt er es sich nicht selbst, z.B. mit this.repo = new Repository()
in seinem Konstruktor, sondern bekommt es stattdessen schon fertig als Parameter übergeben, z.B. public Service(Repository repo)
. Das heißt, obwohl die Abhängigkeit vom Service
zum Repository
geht, wird sie „umgekehrt“ aufgelöst, indem das Repository
dem Service
übergeben wird.
Erklärung
- Wenn Komponenten sich ihre Abhängigkeiten selbst erzeugen, sind diese später nur umständlich oder gar nicht mehr austauschbar.
- Tests von Komponenten, die sich ihre Abhängigkeiten selbst erzeugen, sind schwierig/unmöglich/langsam/fehleranfällig, da die Abhängigkeiten nicht so einfach gegen Fake-Objekte ausgetauscht werden können.
- Die Abhängigkeiten sind nicht explizit in der API sichtbar. Der obige
Service
„braucht“ einRepository
, aber das sieht man nur, wenn man sich den Quelltext vonService
anschaut. Beim Erzeugen einesService
s ist dies nicht offensichtlich, weil er sich dasRepository
„heimlich“ selbst erzeugt.
Beispiel
Um alle Employee
s zu bezahlen, erzeugt sich die Payroll
ihr eigenes MySqlEmployeeRepository
. Dadurch wird auch im Test die echte Datenbank verwendet und ein Austauschen gegen eine Oracle-Datenbank erfordert eine Anpassung der Klasse Payroll
(vgl. Open Closed Principle).
class Payroll def initialize @repo = MySqlEmployeeRepository.new end def pay_employees @repo.find_all.each { |employee| employee.pay } end end class MySqlEmployeeRepository def find_all puts "Accessing the database and reading all employees" [Employee.new] * 10 end end class Employee def pay puts "Thank you for paying me" end end Payroll.new.pay_employees # Accessing the database and reading all employees # Thank you for paying me # ... # Thank you for paying me
Statt dieser „harten“ und versteckten Abhängigkeit sollte Payroll
sich lieber die Implementierung eines Repositorys liefern lassen. Das erfordert nur eine kleine Umstrukturierung des Codes und ermöglicht deutlich einfachere Tests.
class Payroll def initialize(repo) @repo = repo end def pay_employees @repo.find_all.each { |employee| employee.pay } end end class MySqlEmployeeRepository def find_all puts "Accessing the database and reading all employees" [Employee.new] * 10 end end class FakeEmployeeRepository def find_all [Employee.new] * 1 end end class Employee def pay puts "Thank you for paying me" end end puts "Production:" Payroll.new(MySqlEmployeeRepository.new).pay_employees puts "Test:" Payroll.new(FakeEmployeeRepository.new).pay_employees # Production: # Accessing the database and reading all employees # Thank you for paying me # ... # Thank you for paying me # Test: # Thank you for paying me
Außerdem kann Payroll
nun auch ohne Anpassungen (!) mit einer Oracle-Datenbank arbeiten.
class OracleEmployeeRepository def find_all puts "Reading employees from Oracle database" [Employee.new] * 100 end end puts "Using Oracle:" Payroll.new(OracleEmployeeRepository.new).pay_employees # Reading employees from Oracle database # Thank you for paying me # ... # Thank you for paying me
Vorteile
- Abhängigkeiten sind explizit sichtbar und können bei geänderten Anforderungen oder in Tests einfach ausgetauscht werden.
- Neue Funktionalitäten können dem Programm einfacher hinzugefügt werden, da durch die Abhängigkeit von Abstraktionen oftmals auch das OCP umgesetzt wird.
Nachteile
- Ein Problem könnte wieder ein „Overengineering“ sein. Es ist wahrscheinlich nicht sinnvoll, jede Kleinigkeit hinter einer Abstraktion zu verbergen. Aber wie entscheidet man, welche Abstraktion sinnvoll ist?
- Bei vielen Abhängigkeiten wird das Erstellen von Objekten (und damit auch deren Test) schwierig, weil viel Arbeit vorab erledigt werden muss. Hier könnte dann ein DI-Container zum Einsatz kommen.
Literaturempfehlungen
Wie sollte es anders sein: Auch für das letzte SOLID-Prinzip empfehle ich noch einmal Clean Code* von Uncle Bob.
Links
- Permalink zu dieser Podcast-Episode
- RSS-Feed des Podcasts
- Dependency Inversion Principle
- ArticleS.UncleBob.PrinciplesOfOod (Artikel von Uncle Bob, Beispiel in C++)
- Dependency-Inversion-Prinzip – Wikipedia
- Das Dependency Inversion Principle | Informatik Aktuell (Deutsches Beispiel in C#)
Hey,
großartig! Die SOLID-Prinzipien wurden sehr gut und verständlich erklärt und das Wesentliche auf den Punkt gebracht. Zudem wurde alles ansprechend und mit sinnvollen Beispielen verdeutlicht! Einfach klasse!
Vielen Dank und weiter so!
Beste Grüße,
Stefan