Die Begriffe Dependency Injection und Dependency Inversion Principle werden häufig verwechselt, oder die Unterschiede sind nicht klar. Ganz verwirrend wird es, wenn noch der Begriff "Inversion of Control" dazukommt. Wir bringen Licht ins Dunkle.
Neulich wurde ich gefragt, was der Unterschied zwischen Dependency Injection und dem Dependency Inversion Principle ist. Im Prinzip handelt es sich um zwei komplett unterschiedliche Dinge, die man nicht direkt vergleichen kann. Aber: beim Dependency Inversion Principle kommt in der Praxis zumeist Dependency Injection zum Einsatz. Zeit, den Unterschied genau zu beleuchten.
Dependency Inversion Principle
Das Dependency Inversion Principle (DIP) ist, wie der Name schon sagt, ein Prinzip. Um genau zu sein handelt es sich um das letzte der fünf SOLID Prinzipien. Das sind fünf Prinzipien für das Design von objektorientierter Software. Das Ziel der SOLID Prinzipien ist es, das innere Design von Softwareprodukten unkompliziert und erweiterbar zu gestalten.
Für die Masse der Entwickler, die objektorientiert arbeiten, erkläre ich DIP ganz anschaulich an folgendem Beispiel: Ein Autofahrer soll verschiedene Fahrzeuge fahren können: einen Schaltwagen, einen Automatikwagen und ein selbstfahrendes Auto.
Eine mögliche Implementierung (die aber gegen das DIP verstößt!) könnte wie folgt aussehen. Zunächst werfen wir einen Blick auf die Klassen der jeweiligen Autos. Wir fangen mit dem Schaltwagen an, dessen Implementierung für sich gesehen zunächst logisch aussieht. Der Schaltwagen bietet verschiedene Methoden an. Neben der Möglichkeit einen Gang hochzuschalten, gibt es auch Methoden zum "drücken" oder "loslassen" der Kupplung:
public class ManualCar
{
public void Start()
{
// Code...
}
public void Stop()
{
// Code...
}
public void PressClutch()
{
// Code...
}
public void ReleaseClutch()
{
// Code...
}
public void Upshift()
{
// Code...
}
public void Downshift()
{
// Code...
}
}
Das folgende Listing zeigt die Klasse für den Automatikwagen. Auch dieser scheint für sich genommen richtig implementiert. Da unser Fahrer keine Kupplung betätigen kann, gibt es lediglich die Möglichkeit zum Umschalten des sog. "Sportmodus":
public class AutomaticCar
{
public void Start()
{
}
public void Stop()
{
}
public void ChangeMode(bool isSportMode)
{
}
}
Unser autonom fahrendes E-Auto ist der Knaller! Wir müssen es nicht einmal an- oder ausschalten. Das Fahrzeug ist so intelligent, dass es zwischen An und Aus wechselt sobald jemand im Fahrzeug sitzt. Den Elektromotor müssen wir sowieso nicht "starten":
public class SmartCar
{
public void ChangeMode(bool isSportMode)
{
}
}
Spannend wird jetzt die Implementierung des Fahrers. In Abhängigkeit des jeweiligen Fahrzeuges muss der Fahrer unterschiedliche Methoden seines Autos aufrufen. Bedenken Sie bitte, dass ich aktuell noch die Problemstellung verdeutlichen möchte und wir gleich zu einer bessern Lösung kommen.
Die Fahrer-Klasse entscheidet Mittels des Enums, um welche Art von Auto es sich handelt:
public enum Cars
{
ManualCar,
AutomaticCar,
SmartCar
}
public class Driver
{
public Driver(ManualCar manualCar, AutomaticCar automaticCar, SmartCar smartCar)
{
ManualCar = manualCar;
AutomaticCar = automaticCar;
SmartCar = smartCar;
}
public ManualCar ManualCar { get; set; }
public AutomaticCar AutomaticCar { get; set; }
public SmartCar SmartCar { get; set; }
public Cars SelectedCar { get; set; }
public void Start()
{
switch (SelectedCar)
{
case Cars.ManualCar:
{
ManualCar.PressClutch();
ManualCar.Start();
ManualCar.ReleaseClutch();
break;
}
case Cars.AutomaticCar:
{
AutomaticCar.Start();
break;
}
case Cars.SmartCar:
{
// do nothing (starts if driver enteres car)
break;
}
}
}
public void Stop()
{
switch (SelectedCar)
{
case Cars.ManualCar:
{
ManualCar.Stop();
break;
}
case Cars.AutomaticCar:
{
AutomaticCar.Stop();
break;
}
case Cars.SmartCar:
{
// do nothing (stops if driver leaves car)
break;
}
}
}
public void Upshift()
{
switch (SelectedCar)
{
case Cars.ManualCar:
{
ManualCar.PressClutch();
ManualCar.Upshift();
ManualCar.ReleaseClutch();
break;
}
case Cars.AutomaticCar:
{
AutomaticCar.ChangeMode(false);
break;
}
case Cars.SmartCar:
{
SmartCar.ChangeMode(false);
break;
}
}
}
public void Downshift()
{
switch (SelectedCar)
{
case Cars.ManualCar:
{
ManualCar.PressClutch();
ManualCar.Upshift();
ManualCar.ReleaseClutch();
break;
}
case Cars.AutomaticCar:
{
AutomaticCar.ChangeMode(true);
break;
}
case Cars.SmartCar:
{
SmartCar.ChangeMode(true);
break;
}
}
}
}
Leider haben die unterschiedlichen Autos zu einer Art Code Duplikation geführt. Der Code ist nie exakt gleich, aber immer sehr ähnlich. Mittels eines einfachen Extract Method Refactorings lässt sich dieser Code Smell nicht entfernen. Switch-Case Statements sind ein typisches Indiz dafür, dass das Dependency Inversion Principle verletzt ist. Die Anwendung wird schnell unwartbar. Wir müssen perspektivisch überall mit switch-case die Fahrzeuge unterschiedlich ansprechen. Ist Ihnen aufgefallen, dass in der Downshift-Methode des Fahrers beim Schaltwagen die Upshift Methode aufgerufen wird (Copy-Paste Fehler!)? Unsere Realisierung ist also nicht nur schlecht zu warten und zu erweitern, sondern auch noch fehleranfällig.
Das Problem ist übrigens nicht, dass die Autos unterschiedlich sind, sondern dass sich der Fahrer um die jeweilige Unterscheidung kümmern muss. Die Abhängigkeit des Fahrers zu den Autos ist zu stark.

Jetzt ist es Zeit für die Definition des DIP.
Das DIP ist definiert mit:
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.
Vereinfacht ausgedrückt, sagt es aus, dass alle "Teile" unserer Anwendung immer nur von Abstraktionen, aber nicht von der konkreten Realisierung abhängig sein sollten. Für Klassen bedeutet das, dass sie keine Referenzen auf andere konkrete Klassen, sondern zu deren Interfaces besitzen sollen. Sind diese Interfaces nicht vorhanden, müssen sie eingeführt werden. Da es sich um ein Prinzip handelt, sollte es so weit irgendwie möglich immer eingesetzt werden.
Für unser Beispiel bedeutet das, dass wir eine Abstraktion eines Autos einführen:
public interface ICar
{
void Start();
void Stop();
void IncreaseDynamic();
void DecreaseDynamic();
string GetDestination();
}
TIPP: Definieren Sie Abstraktionen immer aus Sicht des Verwenders! Abstraktionen dienen dazu, ein (möglichst sinnhaftes) Verhalten für den Konsumenten bereitzustellen und die Implementierungsdetails hinter einer Sinnvollen Schnittstelle quasi zu "verstecken".
Der Fahrer interessiert sich in unserem Beispiel nicht für die Details des Autos. Er will seinen Zweck erfüllt haben. Unser Fahrer will nicht unbedingt hoch- oder herunterschalten. Was unser Fahrer wirklich will, ist dynamischer oder komfortabler fahren.
Am Beispiel des Schaltwagens zeigt sich, dass trotzdem die fahrzeugspezifische Logik abgebildet werden kann (und ja auch muss). Die Methoden PressClutch() und ReleaseClutch() werden jetzt einfach innerhalb von Increase- bzw. DecreaseDynamic() aufgerufen.
public class ManualCar : ICar
{
public void Start()
{
}
public void Stop()
{
}
public void IncreaseDynamic()
{
PressClutch();
DownShift();
ReleaseClutch();
}
public void DecreaseDynamic()
{
PressClutch();
UpShift();
ReleaseClutch();
}
private void PressClutch()
{
}
private void ReleaseClutch()
{
}
private void UpShift()
{
}
private void DownShift()
{
}
}
public class SmartCar : ICar
{
public void Start()
{
}
public void Stop()
{
}
public void IncreaseDynamic()
{
ChangeMode(true);
}
public void DecreaseDynamic()
{
ChangeMode(false);
}
private void ChangeMode(bool isSportMode)
{
}
}
Der Code der Driver-Klasse erfährt jetzt eine massive Vereinfachung. Er kann einfach nur die Abstraktion verwenden. Das ist pure Eleganz und zeigt die Mächtigkeit des Dependency Inversion Principles:
public class Driver
{
private readonly ICar _car;
public Driver(ICar car)
{
_car = car;
}
public void Start()
{
_car.Start();
}
public void Stop()
{
_car.Stop();
}
public void IncreaseDynamic()
{
_car.IncreaseDynamic();
}
public void DecreaseDynamic()
{
_car.DecreaseDynamic();
}
}
Unser Programm würde sich bei einer neuen Fahrzeugklasse auch problemlos erweitern lassen. Das neue Fahrzeug muss nur ebenso unsere Schnittstelle ICar implementieren und wir müssen nicht hunderte von Stellen, Switch-Case Statements, If-Else, usw. im Programmcode anpassen. Unser High-Level Modul Fahrer ist jetzt nicht mehr von den Low-Level Modulen (den Autos) abhängig. Stattdessen sind beide von der Abstraktion des Autos - dem Interface - abhängig. Der Fahrer ist vom Interface abhängig, da er es anstelle des konkreten Typs verwendet. Die Autos sind vom Interface abhängig, da sie dessen Schnittstelle realisieren.

Die Abhängigkeiten verlaufen jetzt nicht mehr von "oben nach unten" sondern (zumindest teilweise) "bottom to top". Teilweise, weil der Fahrer immer noch von dem Interface abhängig ist. Dieses Interface ist aus komponentenorientierter Sicht auf Ebene der Autos zu sehen. Meine persönliche Meinung ist daher, dass die Abhängigkeiten beim Dependency Inversion Principle nicht umgedreht (to invert) werden, sondern eigentlich eher eine "Umlenkung" über das Interface existiert.

Fakt ist aber, dass die Abhängigkeit zu den konkreten Typen nicht mehr explizit existiert. Man könnte auch von einer schwachen Abhängigkeit sprechen.
Wenn Sie sich für eine ausführliche Erklärung über die SOLID Prinzipien interessieren, dann empfehle ich Ihnen gerne unsere 5 teilige Artikelserie auf informatik-aktuell.de. Mein Kollege Andre Krämer und ich, erklären die SOLID Prinzipien anhand eines durchgängigen Beispiels. Aus didaktischen Gründen fangen wir in der Artikelserie sogar beim Dependency Inversion Principle an.
Dependency Injection
Bei Dependency Injection (DI) handelt es sich nicht um ein Prinzip, sondern vielmehr um eine Technologie. Viele moderne Frameworks wie z.B. Angular bieten Out Of The Box Unterstützung für DI. Das Framework - oder ein dedizierter "DI-Container" - unterstützt uns dabei die Abhängigkeiten in unserer Anwendung flexibel und dynamisch (zur Laufzeit) aufzulösen.
Die Begrifflichkeit "inject" (injizieren) ist entscheidend. Dependency Injection folgt dem "Inversion Of Control" Pattern. Unsere Klassen sollen sich nicht um die Auflösung der Abhängigkeiten kümmern. Stattdessen werden diese, z.B. durch Konstruktorparameter, Getter, Setter usw. vom Dependency Injection Framework an unsere Klasse übergeben. Beim Bootstrappen unserer Anwendung werden dabei Abstraktionen und deren Realisierung am Dependency Injection Framework registriert. Dann kann der DI-Container verwendet werden, um eine Einstiegsklasse o.ä. aufzulösen. Diese besitzt logischerweise Abhängigkeiten zu ersten Abstraktionen. Deren Realisierungen wieder zu anderen Abstraktionen usw. Alle diese Abhängigkeiten werden dann vom DI Container instanziiert und aufgelöst. Der DI Container spannt sozusagen ein ganzes Abhängigkeitsnetz auf. DI besitzt noch mehr Vorteile. Diese behandeln wir z.B. ausführlich in unserer Clean Code Schulung.
Fazit
Durch das Dependency Inversion Principle erhalten wir wartbaren und erweiterbaren Code. Anstelle von starken Abhängigkeiten zu konkreten Typen, besitzen unsere Klassen Abhängigkeiten zu Abstraktionen. Die Abhängigkeiten verschwinden nicht ganz. Sie werden nur schwächer und unser Programm wird wartbar und erweiterbar. Durch Dependency Injection, können die Abhängigkeiten zu Abstraktionen dynamisch, d.h. zur Laufzeit aufgelöst werden.