Dependency Inversion / Inversion of Control / Dependency Injection

https://oer-informatik.de/dependency-inversion

tl/dr; (ca. 4 min Lesezeit): Vom Designprinzip “Dependency Inversion” über die Umsetzung “Inversion of Control” zum Designpattern Dependency Injection.

In der objektorientierten Programmierung (OOP) gibt es eine Reihe von grundlegenden Prinzipien, die die Wartbarkeit, Lesbarkeit und Fehlerresistenz von Code positiv beeinflussen. Diese abstrakten Prinzipien werden Design Principles (Entwurfsprinzipien) genannt. In Anwendung dieser Prinzipen wurden eine Reihe best practices veröffentlicht, die Standardprobleme der OOP lösen und einheitliche Vokabeln für die genutzten Softwarekomponenten festlegen. Diese konkreten Lösungsvorschläge nennt man Design Pattern (Entwurfsmuster). Oft setzen Entwurfsmuster eine Reihe von abstrakten Designprinzipen um - wie hier am Beispiel des Dependency Inversion-Designprinzipts und dem Entwurfsmuster Dependency Injection gezeigt wird.

Das Dependency Inversion Prinzip

Das Dependency Inversion Principle beschäftigt sich mit der Entkopplung von Softwaremodulen untereinander. Vereinfacht gesagt besagt es, dass zwei konkrete Klassen nicht direkt von einandere abhängen sollen (und damit eng gekoppelt sind), sondern die Abhängigkeit über ein Interface (eine abstrakte Struktur) realisiert werden sollte.

Es ist eines der fünf grundlegenden S.O.L.I.D.-Prinzipien, die Robert C. Martin in seinem Clean Code-Buch veröffentlicht hat.

Kokreter besagt es, dass high level-Module nicht direkt von low level-Modulen abhängen sollen. Diese Abhängigkeit lässt sich im UML-Klassendiagramm folgendermaßen darstellen:

UML-Klassendiagramm Abhängikeit zwischen Client und ConcreteService
UML-Klassendiagramm Abhängikeit zwischen Client und ConcreteService

Ein Beispiel mit Quelltext. Wir betrachten einen High-Level Client, der eine Instanz des Low-Level Services erzeugt und nutzt (hier: als Attribut). Der Client hängt von der Implementierung des Services ab:

Die Abhängigkeit in diesem Beispiel ist konkret eine einseitig navigierbare Assoziation:

UML-Klassendiagramm der Assoziation zwischen Client und ConcreteService
UML-Klassendiagramm der Assoziation zwischen Client und ConcreteService

Das Dependency Inversion Prinzip besagt:

High-Level Module sollen nicht von Low-Level-Modulen abhängen. Beide sollen von Abstraktionen abhängen.

Die Abhängigkeit soll also aufgelöst werden, in dem ein Interface die Abhängigkeit zwischen Client und Server entkoppelt. Das Interface wird von der High-Level-Klasse genutzt und von der Low-Level-Klasse implementiert:

Im UML-Klassendiagramm bleibt die einseitig navigierbare Assoziation erhalten, zielt jedoch auf das Interface. Auch die Abhängigkeit des ConcreteService (Implementierung) zeigt auf das Interface. Beide hängen also nur noch von der abstrakten Struktur des ServiceInterface ab.

UML-Klassendiagramm mit Client, ServiceInterface und ConcreteService
UML-Klassendiagramm mit Client, ServiceInterface und ConcreteService

Wenn Assoziationen über Interfaces realisiert werden, werden diese in UML-Klassendiagrammen häufig in der “Lollipop”-Notation dargestellt, die das Interface nicht mehr ausführlich, sondern nur noch als Stecker/Buchse skizziert darstellt:

UML-Klassendiagramm mit Lollipopp-Notation zwischen Client und ConcreteService
UML-Klassendiagramm mit Lollipopp-Notation zwischen Client und ConcreteService

Objekterzeugung entkoppeln: Inversion of Control (IoC)

Ein weiterer Schritt der Entkopplung wird unter dem Prinzip Inversion of Control (IoC) zusammengefasst:

Die Kontrolle darüber, welche konkrete Implementierung eines Service aufgerufen wird, wird vom eigentlichen Aufruf der Methode entkoppelt - die Konrolle also umgekehrt.

Die Erzeugung einer Objektinstanz einer abstrahierten Klasse (hier: ServiceInterface) soll von deren Nutzung (hier: durch Client) entkoppelt werden. Wir übergeben die Kontrolle über die Objektinstanz nach außen (z.B. über Setter oder Konstruktoren):

UML-Klassendiagramm ergänzt um Konstruktor und Setter-Methode, die das ConcreteService-Objekt übergibt
UML-Klassendiagramm ergänzt um Konstruktor und Setter-Methode, die das ConcreteService-Objekt übergibt

Im Code einer Client-Klasse soll also nicht selbst festlegt werden, welche Klassen (oder Bibliotheken) die aufgerufenen Methoden des ServiceInterface implementieren, sondern dies soll an anderer Stelle (i.d.R. durch ein Framework) erfolgen.

Dies setzt zunächst das dependency inversion principle (DIP) voraus.

Im einfachsten Fall geschieht dies über Parameter, die bei Methodenaufrufen von extern übergeben werden.

Ein Beispiel mit Konstruktoren:

oder ein Beispiel mit Setter-Methoden:

Das konkrete Verhalten des jeweiligen Methodenaufrufs kann somit außerhalb der Klasse geändert werden - abhängig davon, welche konkrete Implementierung mit dem Methodenaufruf später verknüpft wird. Hierzu werden in den implementierten Codeabschnitten abstrakte Strukturen genutzt, die beispielsweise im Framework implementiert werden müssen.

Vorteil dieser Technik ist eine Entkopplung des Methodenaufrufs von der Implementierung, damit verbunden

  • bessere Modularität, Klassen können beispielsweise auch leicht getauscht werden

  • leichter Testbarkeit, da Abhängigkeiten leichter über Mock-Objekte darstellbar sind

Praktisch umgesetzt werden kann IoC über die Design Pattern:

  • Dependency Injection

  • Factory Pattern

  • Strategy Pattern

  • Service Locator Pattern

Dependency Injection

Bei dependency injection (DI) wird die Übergabe der konkreten Instanz nicht mehr (wie noch im obigen Beispiel) über die Implementierung des Codes selbst umgesetzt, sondern durch das Framework. Neben den beiden Klassen Client und Service, die per DIP entkoppelt wurden, gibt es nun einen weiteren Akteur: den Injector, der i.d.R. im Framework implementiert ist.

UML-Klassendiagramm mit ergänzter Injector-Klasse
UML-Klassendiagramm mit ergänzter Injector-Klasse

Hierbei werden drei unterschiedliche Arten der Injezierung genutzt:

  • constructor injection: Die konkrete Instanz wird als Parameter eines Konstruktors übergeben

  • setter injection: Die konkrete Instanz wird als Parameter eines Setters übergeben

  • field injection

  • https://www.baeldung.com/inversion-control-and-dependency-injection-in-spring
  • https://stackify.com/dependency-injection/
  • https://martinfowler.com/articles/injection.html

Quellen und offene Ressourcen (OER)

Die Ursprungstexte (als Markdown), Grafiken und zugrunde liegende Diagrammquelltexte finden sich (soweit möglich in weiterbearbeitbarer Form) in folgendem git-Repository:

https://gitlab.com/oer-informatik/design-pattern/dependency-injection/.

Sofern nicht explizit anderweitig angegeben sind sie zur Nutzung als Open Education Resource (OER) unter Namensnennung (H. Stein, oer-informatik.de) freigegeben gemäß der Creative Commons Namensnennung 4.0 International Lizenz (CC BY 4.0).

Creative Commons Lizenzvertrag

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: