Eine Injektion beim Arzt zu erhalten wird von vielen Menschen als sehr unangenehm empfunden; Personen, die unter einer Nadelphobie leiden, bekommen sogar Herzrasen, Schwindelgefühle und Panikattacken. Das Injizieren von Abhängigkeiten (engl.: Dependency Injection, DI) im Software-Design hingegen ist völlig schmerzfrei, löst die starke Kopplung und Verdrahtung von Abhängigkeiten, schafft überschau- und wartbaren Code und fördert die Wiederverwendbarkeit.

Das Problem: zu viele Abhängigkeiten

In einem großen Software-Projekt sind eine Vielzahl von Abhängigkeiten zwischen einzelnen Softwarebausteinen, wie beispielsweise Klassen, unvermeidbar. Das muss man einfach als systemimmanentes Faktum hinnehmen und ist auch grundsätzlich kein Problem. Schließlich ergibt ja erst das koordinierte Zusammenspiel der Klassen ein funktionierendes Software-System, und dafür müssen sich diese Klassen nun mal bis zu einem gewissen Grad kennen.

Unangenehm wird es dann, wenn zu viele und zum Teil auch unnötige Abhängigkeiten existieren, so dass die Wartbarkeit des Systems rapide sinkt. Plötzlich haben selbst kleine Änderungen unvorhersehbare Nebeneffekte, neue Fehler schleichen sich ein und das System wird fragil. Der Aufwand für Modifikationen ist nur noch schwer abschätzbar, weil man nicht genau weiß, wie viele Komponenten man „anfassen“ und ändern muss. Zudem wird die isolierte Testbarkeit von Software-Einheiten (Module, Klassen, …) erheblich erschwert, mitunter sogar unmöglich. Die Management-Vorgabe in einem solchen Umfeld lautet daher meistens: „Never touch a running system!“

Zur Veranschaulichung einer sehr starken Abhängigkeit und der damit verbundenen Problematik, schauen wir uns mal ein kleines C++ Code-Beispiel an. Als erstes haben wir eine Klasse CustomerDataAccessObject, die Kundendaten in einer relationale Datenbank anlegen, herauslesen, ändern und auch löschen kann:

Die Schnittstelle unserer Klasse Customer sieht folgendermaßen aus:

Der Zeiger auf das DAO in Zeile 18 wird beispielsweise u.a. dazu genutzt, ein Kundenobjekt mit Daten aus der Datenbank zu befüllen, wie der folgende Ausschnitt aus der Implementierung zeigt (ein entsprechender Zuweisungs-Operator existiert):

Als UML-Klassendiagramm würde die Struktur wie folgt aussehen (bitte anklicken für Vergrößerung):

class-diagram

Dieser Code ist grundsätzlich nicht völlig falsch! Er dürfte das tun, was man von ihm erwartet, nämlich: das Objekte der Klasse Customer Objekte vom Typ CustomerDataAccessObject dazu verwenden, um Kundendaten aus einer Datenbank zu laden, in der Datenbank zu speichern, etc.

Dem gegenüber ist das Design in mehrfacher Hinsicht problematisch. Vor allem ist die starke Kopplung zwischen der Kundenklasse und dem DAO sehr störend.

Zum einen sieht man, dass die Konstruktion und Destruktion des DAO in der Verantwortlichkeit der Kundenklasse liegt (siehe hervorgehobene Zeilen 7 und 11 in Customer.cpp). Abgesehen davon, dass man in einem modernen C++ Programm sowieso keine „rohen“ Zeiger mehr verwenden sollte, sollte es auch nicht Aufgabe der Kundenklasse sein, das Ressourcenmanagement für das DAO durchzuführen.

Zudem ist die Erweiterbarkeit stark eingeschränkt, denn sobald die Anforderung gestellt wird, das man die Kundendaten beispielsweise auch in einem Dateisystem laden bzw. speichern können muss, ist die direkte Abhängigkeit von Customer zu CustomerDataAccessObject sehr störend.

Darüber hinaus ist das Unit-Testing von Customer.cpp de facto unmöglich, denn der Kunde ist nicht ohne das Datenbank-Zugriffsobjekt autonom testbar. Zugriffe auf externe Systeme, wie Datenbanken, sind beim Unit-Testing nicht erlaubt. Man würde ja nicht nur seine eigene Unit prüfen, sondern auch anderen Code und die Funktion der Datenbank. Zudem sind Zugriffe auf Datenbanken langsam und sie können subtile Seiteneffekte hervorrufen. Und was will man tun, wenn man in einer Umgebung testet, in der man gar nicht über eine Datenbank verfügt und CustomerDataAccessObject somit keine Verbindung aufbauen kann?

Die Lösung: Abhängigkeiten externalisieren

An dieser Stelle kommt nun Dependency Injection ins Spiel. Die Idee ist, das man die Klasse Customer nicht unmittelbar abhängig von einer konkreten Implementierung für eine Persistenzlösung macht, sondern das man diese Abhängigkeit von außen zur Laufzeit in Customer „injizieren“ kann.

Zu diesem Zweck wandeln wir CustomerDataAccessObject zunächst in eine abstrakte Basisklasse um, die frei von jeglicher Implementierung ist. Das geschieht dadurch, dass wir alle öffentlichen Methoden in pure virtual functions umwandeln:

Von dieser abstrakten Basisklasse leiten wir nun ein konkretes DAO ab, in das unsere ursprüngliche Implementierung für den Datenbankzugriff hineinwandert:

Nun wird Customer so abgeändert, das es eine Referenzvariable vom Typ CustomerDataAccessObject besitzt, und das man bei der Erzeugung einer Instanz von Customer eine Instanz vom Typ CustomerDataAccessObject als Konstruktor-Parameter mitgibt:

In diesem Fall ist auch der Default-Konstruktor privat deklariert worden, um die Verwendung des Initialisierungs-Konstruktors zu erzwingen. Das sorgt dafür, dass keine Instanzen von Customer ohne gültige Referenz auf ein DAO erzeugt werden können. Darüber hinaus kann die Methode loadFromDatabaseSelectedByIdentifier in loadDataSelectedByIdentifier umbenannt werden. Der Name einer Methode sollte sowieso ausdrücken, was die Methode macht, und nicht wie sie etwas macht. Die Implementierung des DAO ist ja jetzt auch austauschbar, d.h. es ist möglicherweise nicht immer eine Datenbank im Spiel.

Ein Blick in die zugehörige Implementierung zeigt darüber hinaus auch, das die Konstruktion/Destruktion des DAO mittels new bzw. delete weggefallen ist:

Enchilada

syringe-wtih-codeDie Instanziierung der Klassen und der Aufbau des Abhängigkeitsnetzes muss nun von einem anderen Objekt verantwortet werden. Dieses Objekt bzw. diese Komponente nennt man Injizierer, oder Assembler (to assemble, engl. für: zusammenfügen, montieren).

Der Assembler kennt quasi den „Bauplan“ der Software und muss bei der Erzeugung von Customer diesem mittels des Konstruktors ein Objekt hineinreichen („injizieren“), welches ein Untertyp der abstrakten Klasse CustomerDataAccessObject ist, und dessen virtuellen Methoden überschreibt und implementiert. In unserem Fall wäre es beispielsweise eine Instanz der Klasse CustomerDataAccessThroughDatabase. Dieses Vorgehen, dass man Abhängigkeiten auflöst in dem man unabhängige Objekte den Konstruktoren der abhängigen Objekte zur Verfügung stellt, nennt man Constructor Injection:

CustomerDataAccessThroughDatabase ist nur eines von vielen denkbaren, konkreten DAOs. So ist beispielsweise eine weitere Klasse CustomerDataAccessThroughFilesystem vorstellbar, mit der man die Kundendaten in ein Dateisystem schreiben bzw. davon lesen kann. Eine Klasse CustomerDataAccessMock kann als Testattrappe (test double) dienen und somit das Unit-Testing von Customer ohne Datenbanken und anderen externen Systemen ermöglichen.

Das folgende UML-Klassendiagramm zeigt das neue Design mit verschiedenen Datenzugriffsobjekten:

class-diagram

Darüber hinaus wäre es auch noch möglich, das man Customer um eine Setter-Methode für Datenzugriffsobjekte erweitert (sog. Setter Injection). Der Vorteil von Setter Injection ist das die Art und Weise, wie Customer Daten lädt und/oder persistiert, zur Laufzeit austauschbar wäre:

Selbstverständlich lassen sich auch Constructor- und Setter-Injection kombinieren.

Fazit

Wie man sieht, ist Dependency Injection eine effektive Methode, um das Design von Software flexibel zu machen, störende Abhängigkeiten zu eliminieren, dadurch die lose Kopplung zu fördern, und die isolierte Testbarkeit von Code zu erleichtern. Objekte lassen sich nun zur Laufzeit quasi „zusammenstecken“ und können voneinander auch wieder gelöst und neu konfiguriert werden, was beispielsweise die Bildung von Varianten von ein- und derselben Applikation ermöglicht. DI unterstützt in hervorragender Weise das Open-Closed Principle (OCP).

„Dependency Injection is a key element of agile architecture.“ (Ward Cunningham)

DI ist eines der zentralen Konzepte in sog. Inversion of Control (IoC) Frameworks, wie z.B. PocoCapsule für C++.

Dependency Injection
Markiert in:                                

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.