Dependency Injection
Abhängigkeiten von außen übergeben
Die Idee und ihr Nutzen
Ein Programmteil einer Versicherung gibt Schäden frei. Dafür braucht der Programmteil zwei Helfer: einen Speicher, der den Schaden lädt und speichert, und einen Benachrichtiger, der dem Kunden eine Nachricht schickt. Erzeugt der Programmteil diese Helfer selbst, steht in seinem Code fest, dass er die echte Datenbank und den echten Mailversand benutzt, und jeder Test verschickt echte E-Mails. Stattdessen bekommt der Programmteil seine Helfer von außen übergeben. Im Betrieb übergibt ein eigener Programmteil beim Start der Anwendung die echten Helfer. Ein automatischer Test übergibt dagegen Ersatzhelfer, die ohne Datenbank auskommen und keine E-Mails verschicken. Der Programmteil selbst bleibt in beiden Fällen unverändert. Solche Helfer heißen auch Abhängigkeiten des Programmteils, weil der Programmteil ohne sie nicht arbeiten kann, und das Übergeben von außen heißt Dependency Injection, auf Deutsch Einsetzen von Abhängigkeiten.
Wie funktioniert es?
Am Morgen startet die Schadenanwendung der Versicherung. Beim Start erzeugt ein Container alle Programmteile der Schadenanwendung und übergibt ihnen ihre Helfer. Der Container ist ein Teil des verwendeten Programmrahmens, also des fertigen Codes, auf dem die Schadenanwendung aufbaut. Einer der Programmteile ist die SchadenFreigabe, die Schäden freigibt. Außerdem erzeugt der Container den DatenbankSchadenSpeicher, der die Schadendatenbank liest und beschreibt, und den MailBenachrichtiger, der über den Mailserver der Versicherung sendet. Diese beiden Helfer übergibt der Container der SchadenFreigabe beim Erzeugen. Eine Sachbearbeiterin gibt Schaden 4711 frei. Die SchadenFreigabe lädt den Schaden über den DatenbankSchadenSpeicher, der den Schaden aus der Schadendatenbank liefert. Die SchadenFreigabe prüft die Regeln, speichert die Freigabe über den DatenbankSchadenSpeicher und lässt den Kunden über den MailBenachrichtiger per E-Mail benachrichtigen, ohne zu wissen, wie dieser Helfer das macht. Im automatischen Test prüft das Team der Schadenanwendung dieselbe SchadenFreigabe. Der automatische Test erzeugt die SchadenFreigabe selbst und übergibt ihr einen SpeicherImArbeitsspeicher, der einen vorbereiteten Schaden nur in einer Liste hält, und einen MitschreibenderBenachrichtiger, der keine E-Mails verschickt, sondern nur notiert, welche Nachricht er hätte senden sollen. Der automatische Test lässt die SchadenFreigabe den vorbereiteten Schaden freigeben und prüft danach, ob der MitschreibenderBenachrichtiger genau eine Nachricht an den richtigen Kunden notiert hat. Der automatische Test läuft in Sekundenbruchteilen, ohne Datenbank und ohne dass eine echte E-Mail verschickt wird. Ein Jahr später soll der Kunde statt einer E-Mail eine SMS erhalten. Das Team schreibt einen SmsBenachrichtiger und stellt den Container so ein, dass er den SmsBenachrichtiger statt des MailBenachrichtiger übergibt. Die SchadenFreigabe bleibt unverändert.
Die SchadenFreigabe enthält eine Fachregel, nämlich wann ein Schaden freigegeben werden darf. Ihre Helfer arbeiten mit Technik außerhalb des Programms, mit der Schadendatenbank und dem Mailserver. Das Team legt im Code der SchadenFreigabe nur fest, was die SchadenFreigabe braucht: etwas, das Schäden laden und speichern kann, und etwas, das Kunden benachrichtigen kann. Diese Beschreibungen heißen Schnittstellen, hier SchadenSpeicher und Benachrichtiger. Wie ein Helfer seine Aufgabe erledigt, lassen die Schnittstellen offen. Angeboten werden die Schnittstellen von den Helfern: DatenbankSchadenSpeicher und SpeicherImArbeitsspeicher erfüllen die Schnittstelle SchadenSpeicher. MailBenachrichtiger, MitschreibenderBenachrichtiger und SmsBenachrichtiger erfüllen die Schnittstelle Benachrichtiger. Die SchadenFreigabe ruft ihre Helfer über die Schnittstellen auf und kennt nicht die konkreten Helfer.
Die Schadenanwendung ist in einer objektorientierten Sprache wie Java oder C# geschrieben. In solchen Sprachen werden Programmteile als Objekte erst beim Programmlauf erzeugt. Die Helfer werden der SchadenFreigabe meist über ihren Konstruktor übergeben, also den Teil des Codes, der beim Anlegen eines Programmobjekts ausgeführt wird. Die SchadenFreigabe speichert die übergebenen Helfer und benutzt sie später. Welche konkreten Helfer eingesetzt werden, entscheidet ein Programmteil außerhalb der SchadenFreigabe. In großen Anwendungen, die wie die Schadenanwendung aus vielen Programmteilen bestehen, ist das oft ein Container. In kleinen Anwendungen kann auch die Startroutine des Programms die Helfer von Hand übergeben. Im Test entscheidet der automatische Test selbst, welche Helfer die SchadenFreigabe bekommt.
Findet der Container für eine Schnittstelle keinen oder mehr als einen passenden Helfer, bricht der Container mit einer Fehlermeldung ab. Das geschieht meist schon beim Start, manchmal erst, wenn ein Programmteil zum ersten Mal erzeugt wird. Soll die Schadenanwendung je Kunde zwischen E-Mail und SMS wählen, genügt das Umstellen des Containers nicht. Dann braucht die Schadenanwendung eine zusätzliche fachliche Regel, welcher Kunde welche Benachrichtigungsart erhält. Diese Entscheidung trifft weder der Container noch die Startroutine.
Dependency Injection: dieselbe Freigabe mit echten und mit Test-Helfern
Wann ist es sinnvoll und wann nicht?
Dependency Injection lohnt sich
- Eine Anwendung hat mehr als eine Handvoll Programmteile, die einander brauchen. Ohne Dependency Injection müsste jeder dieser Programmteile seine Helfer selbst erzeugen und dafür wissen, wie jeder Helfer angelegt wird.
- Helfer sollen je Umgebung wechseln, im Test andere als im Betrieb. Erzeugt die
SchadenFreigabeihre Helfer selbst, steht in ihrem Code „Lege eine Verbindung zur Schadendatenbank an“ und „lege einen Mailversand über den Mailserver der Versicherung an“. Dann braucht jeder Test eine laufende Datenbank und verschickt echte E-Mails an die hinterlegten Adressen. Im schlimmsten Fall erhalten echte Kunden Testnachrichten. - Ein Helfer wird voraussichtlich ausgetauscht. Ein Beispiel ist der Wechsel vom Mailversand zum SMS-Versand. Erzeugt die
SchadenFreigabeden Mailversand selbst, muss jemand ihren Code ändern, obwohl sich an der Freigaberegel nichts geändert hat. Mit Dependency Injection kommt nur ein neuer Helfer hinzu, und die Einstellung des Containers ändert sich. - Programmteile sollen offen zeigen, wovon sie abhängen. Der Konstruktor der
SchadenFreigabezeigt auf einen Blick, dass sie einenSchadenSpeicherund einenBenachrichtigerbraucht. Erzeugt dieSchadenFreigabeihre Helfer selbst, sieht man erst beim Lesen ihres ganzen Codes, dass sie die Schadendatenbank und den Mailserver benutzt.
Dependency Injection ist nicht das Richtige
- Eine Hilfsfunktion hat keine eigenen Helfer. Ein Beispiel ist eine Funktion, die Beträge auf Cent rundet. Dann ruft die
SchadenFreigabedie Funktion direkt auf. Eine Schnittstelle und ein Eintrag im Container entfallen, und ein Test verwendet die echte Funktion, weil sie weder Datenbank noch Mailserver braucht. - Eine kleine Anwendung besteht nur aus wenigen Programmteilen. Dann ist ein Container nicht das Richtige, denn er würde nur wenige Übergaben erledigen, und das Team müsste trotzdem seine Regeln kennen. Stattdessen erzeugt die Startroutine die Programmteile von Hand und übergibt sie einander, so wie es Dependency Injection auch ohne Container erlaubt. In einem sehr kleinen Programm genügt es oft, einer Funktion den Helfer bei jedem Aufruf mitzugeben. Jede Übergabe steht dann sichtbar im Code der Startroutine oder des Aufrufs.
- Ein vorhandener Programmrahmen erzeugt die Objekte selbst und erlaubt keine Übergabe über den Konstruktor. Dann erfragt die
SchadenFreigabeihre Helfer bei einem Service Locator. Ein Service Locator ist ein zentrales Verzeichnis, das zu jeder Schnittstelle den passenden Helfer liefert. Beim Start trägt die Startroutine die echten Helfer in das Verzeichnis ein, ein automatischer Test trägt seine Ersatzhelfer ein. So lassen sich Helfer auch mit einem Service Locator austauschen. Jede Abfrage steht im Code derSchadenFreigabe, und wer den Code liest, sieht dort, woher jeder Helfer kommt. Dafür hängt dieSchadenFreigabevom Verzeichnis ab, und jeder Test muss das Verzeichnis vorbereiten.
Nachteile und typische Fehler
Dependency Injection regelt nur, wer der SchadenFreigabe welchen Helfer übergibt. Ob ein Helfer richtig arbeitet, prüft das Verfahren nicht. Container setzen Helfer oft automatisch nach Regeln ein, etwa „übergib den einzigen Helfer, der die Schnittstelle Benachrichtiger erfüllt“. Diese Regeln stehen nicht im Code der SchadenFreigabe. Wer den Code liest, erkennt dann schwer, welcher Helfer tatsächlich verwendet wird. Das Team bindet die Anwendung außerdem an den Container ihres Programmrahmens und muss die Regeln dieses Containers kennen. Für jeden Ersatzhelfer im Test entsteht zudem Code, der gepflegt werden muss, wenn sich die Schnittstelle ändert.
Typische Fehler und woran man sie erkennt:
- Falscher Helfer im Betrieb: Der
MitschreibenderBenachrichtigerliegt versehentlich nicht nur im Testcode, sondern auch in der Schadenanwendung. Eine Regel des Containers, die im Code derSchadenFreigabenicht zu sehen ist, wählt ihn statt desMailBenachrichtiger. Der Container übergibt derSchadenFreigabedann auch im Betrieb denMitschreibenderBenachrichtiger. Kunden erhalten keine Nachricht, und es gibt keine Fehlermeldung. Erkennbar ist das an Freigaben, zu denen der Mailserver keinen Versand verzeichnet. - Späte Zuordnungsfehler: Für eine Schnittstelle fehlt ein Helfer, oder es gibt zwei passende. Der Fehler zeigt sich erst beim Programmstart oder im laufenden Betrieb, nicht schon beim Übersetzen des Codes in ein lauffähiges Programm. Erkennbar ist das an Abbrüchen beim Programmstart oder beim ersten Erzeugen eines Programmteils mit Meldungen über fehlende oder mehrdeutige Helfer.
- Zu viele Schnittstellen: Für jede Kleinigkeit gibt es eine eigene Schnittstelle mit nur einem Helfer. Erkennbar ist das, wenn man für eine einzige Freigabe viele Dateien öffnen muss, um zu sehen, welcher Code tatsächlich läuft.
- Helfer doch selbst geholt: Die
SchadenFreigabefragt den Container in ihrem eigenen Code nach Helfern, statt sie übergeben zu bekommen. Erkennbar ist das an Tests, die den ganzen Container starten müssen, um die Freigabe zu prüfen. - Abweichender Ersatzhelfer: Ein Ersatzhelfer verhält sich nicht automatisch wie der echte Helfer. Der
SpeicherImArbeitsspeichernimmt zum Beispiel Schäden an, die die echte Schadendatenbank ablehnen würde. Erkennbar ist das an Fehlern im Betrieb bei Freigaben, die in den Tests gelungen sind.
Weiterführend
Verwandte Ansätze
- Inversion of Control: der allgemeine Grundsatz, dass ein Programmteil nicht selbst steuert, wann und womit er arbeitet. Dependency Injection ist eine Form davon.
- Hexagonal Architecture und Clean Architecture: Bauweisen, die Fachregeln von Technik trennen. Beide nutzen Dependency Injection, um technische Teile wie den Datenbankzugriff beim Start einzusetzen.
- Repository: eine Schnittstelle zum Laden und Speichern fachlicher Objekte, wie der
SchadenSpeicher. Ein Repository wird typischerweise per Dependency Injection übergeben. - Separation of Concerns: der Grundsatz, dass jeder Programmteil nur eine Aufgabe hat. Die
SchadenFreigabekümmert sich um die Freigabe, nicht darum, ihre Helfer zu erzeugen.
Werkzeuge und Umsetzungen
- Spring Framework: verbreiteter Programmrahmen für Java, dessen zentraler Bestandteil ein Container für Dependency Injection ist.
- Google Guice: Container für Java, der ohne einen großen Programmrahmen auskommt.
- Dagger: legt die Zuordnung der Helfer für Java und Android schon beim Übersetzen des Codes fest, statt sie erst beim Programmstart zu ermitteln. Zuordnungsfehler fallen so früher auf.
- Weld: Umsetzung des Java-Standards CDI, der Dependency Injection für Java-Anwendungen festlegt.
- Microsoft.Extensions.DependencyInjection: der eingebaute Container der Plattform .NET.
Quellen
- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern: Aufsatz von 2004, der den Begriff Dependency Injection prägt, die Übergabe über den Konstruktor beschreibt und das Verfahren mit dem Service Locator vergleicht.
- Spring Framework Reference: Dependency Injection: offizielle Dokumentation eines verbreiteten Containers; beschreibt die Übergabe über den Konstruktor und wann der Container fehlende Helfer erkennt.