Anıl Şenocak

Dependency Injection ist ein Ansatz zur Implementierung einer losen Kopplung (loose coupling) zwischen Klassen in einer Anwendung. Es gibt verschiedene Möglichkeiten, Abhängigkeiten zu injizieren, und dieser Artikel erklärt, warum Constructor Injection der bevorzugte Weg sein sollte.
Dependency: Ein Objekt benötigt oft Objekte anderer Klassen, um seine Operationen auszuführen. Diese Objekte nennen wir Abhängigkeiten.
Injection: Der Prozess des Bereitstellens der erforderlichen Abhängigkeiten für ein Objekt.
Daher hilft Dependency Injection bei der Implementierung der Umkehrung der Kontrolle (Inversion of Control, IoC). Dies bedeutet, dass anstelle der Klasse, die die Abhängigkeitsobjekte erstellt, die Verantwortung für die Objekterstellung und das Injizieren von Abhängigkeiten an das Framework (z. B. Spring) übergeben wird.
Wir können Dependency Injection mit folgenden Ansätzen implementieren:
Die für die Klasse erforderlichen Abhängigkeiten werden dem Konstruktor als Argumente übergeben:
@Component
class Cake {
private Flavor flavor;
Cake(Flavor flavor) {
Objects.requireNonNull(flavor);
this.flavor = flavor;
}
Flavor getFlavor() {
return flavor;
}
//...
}
Vor Spring 4.3 mussten wir dem Konstruktor eine @Autowired-Annotation hinzufügen. In neueren Versionen ist dies optional, wenn die Klasse nur eine einzige Konstruktormethode hat. Da wir in der obigen Klasse Cake nur einen Konstruktor haben, müssen wir die @Autowired-Annotation nicht angeben. Betrachten Sie die folgenden zwei Konstruktor-Beispiele:
@Component
class Sandwich {
private Topping toppings;
private Bread breadType;
Sandwich(Topping toppings) {
this.toppings = toppings;
}
@Autowired
Sandwich(Topping toppings, Bread breadType) {
this.toppings = toppings;
this.breadType = breadType;
}
//...
}
Wenn wir eine Klasse mit mehreren Konstruktormethoden haben, müssen wir die @Autowired-Annotation explizit an einem der Konstruktoren hinzufügen, damit Spring weiß, welcher Konstruktor zum Injizieren der Abhängigkeiten verwendet werden soll.
Wir injizieren die erforderlichen Abhängigkeiten als Parameter in die Klasse, und die Werte werden über die "Setter"-Methoden der Eigenschaften festgelegt. Wir müssen die Setter-Methoden mit der @Autowired-Annotation versehen. Die Klasse Cookie benötigt ein Objekt vom Typ Topping. Das Topping-Objekt wird als Argument in der Setter-Methode dieser Eigenschaft übergeben:
@Component
class Cookie {
private Topping toppings;
@Autowired
void setTopping(Topping toppings) {
this.toppings = toppings;
}
Topping getTopping() {
return toppings;
}
//...
}
Spring findet die @Autowired-Annotation und ruft den Setter auf, um die Abhängigkeit zu injizieren.
Spring weist die erforderlichen Abhängigkeiten direkt den mit @Autowired annotierten Feldern zu. In diesem Beispiel erlauben wir Spring, die Topping-Abhängigkeit über Field Injection zu injizieren:
@Component
class IceCream {
@Autowired
private Topping toppings;
Topping getToppings() {
return toppings;
}
void setToppings(Topping toppings) {
this.toppings = toppings;
}
}
Was passiert, wenn wir @Autowired sowohl einem Feld als auch einem Setter hinzufügen? Welche Methode verwendet Spring, um die Abhängigkeit zu injizieren?
@Component
class Pizza {
@Autowired
private Topping toppings;
Topping getToppings() {
return toppings;
}
@Autowired
void setToppings(Topping toppings) {
this.toppings = toppings;
}
}
Im obigen Beispiel haben wir die @Autowired-Annotation sowohl der "Set"-Funktion als auch dem Feld hinzugefügt. In diesem Fall injiziert Spring die Abhängigkeit über die Setter-Injektionsmethode. Beachten Sie, dass das Mischen von Injektionsarten in einer einzigen Klasse eine schlechte Praxis (Bad Practice) ist, da es den Code weniger lesbar macht.
Nachdem wir nun die verschiedenen Injektionsarten kennengelernt haben, werfen wir einen Blick auf einige Vorteile der Verwendung von Constructor Injection.
Wir erstellen das Objekt, wenn die Konstruktormethode aufgerufen wird. Wenn die Konstruktormethode alle erforderlichen Abhängigkeiten als Parameter erwartet, können wir zu 100 % sicher sein, dass die Klasse niemals instanziiert wird, ohne dass ihre Abhängigkeiten injiziert wurden.
Der IoC-Container stellt sicher, dass alle an die Konstruktormethode übergebenen Argumente verfügbar sind, bevor sie an den Konstruktor übergeben werden. Dies hilft, die berüchtigte NullPointerException zu vermeiden.
Constructor Injection ist äußerst nützlich, da die Codekomplexität vereinfacht wird, weil wir nicht überall separate Geschäftslogik schreiben müssen, um zu prüfen, ob alle erforderlichen Abhängigkeiten geladen sind.
Was ist mit optionalen Abhängigkeiten? Bei der Setter-Injektion ermöglicht uns Spring, optionale Abhängigkeiten zu definieren, indem wir eine Setter-Methode mit @Autowired(required=false) versehen. Dies ist bei der Constructor Injection nicht möglich, da required=false auf alle Konstruktorargumente angewendet werden würde.
Wir können jedoch weiterhin optionale Abhängigkeiten bei der Constructor Injection bereitstellen, indem wir den Typ Optional von Java verwenden.
Constructor Injection hilft uns festzustellen, ob unsere Beans von zu vielen anderen Objekten abhängen. Wenn unser Konstruktor eine große Anzahl von Argumenten hat, kann dies ein Zeichen dafür sein, dass unsere Klasse zu viele Verantwortlichkeiten hat. Wir sollten in Erwägung ziehen, unseren Code zu refaktorisieren, um eine bessere Trennung der Zuständigkeiten (Separation of Concerns) zu erreichen.
Constructor Injection vereinfacht das Schreiben von Unit-Tests. Der Konstruktor zwingt uns dazu, gültige Objekte für alle Abhängigkeiten bereitzustellen. Mithilfe von Mocking-Bibliotheken wie Mockito können wir Mock-Objekte erstellen, die wir dann an den Konstruktor übergeben können.
Natürlich können wir Mock-Daten auch über Setter übergeben. Wenn wir jedoch einer Klasse eine neue Abhängigkeit hinzufügen, vergessen wir möglicherweise, den Setter im Test aufzurufen, was potenziell zu einer NullPointerException im Test führt. Constructor Injection stellt sicher, dass unsere Testfälle nur ausgeführt werden, wenn alle Abhängigkeiten vorhanden sind.
Constructor Injection hilft bei der Erstellung unveränderlicher (immutabler) Objekte, da die Signatur eines Konstruktors die einzig mögliche Art der Objekterstellung darstellt. Sobald wir eine Bean erstellt haben, können wir ihre Abhängigkeiten nicht mehr ändern. Bei der Setter-Injektion ist es möglich, die Abhängigkeit nach der Erstellung zu injizieren, was unter anderem zu veränderlichen Objekten führt, die in einer Multithreading-Umgebung möglicherweise nicht threadsicher sind und sich aufgrund ihrer Veränderlichkeit schwerer debuggen lassen.
Constructor Injection macht den Code robuster. Sie ermöglicht es uns, unveränderliche Objekte zu erstellen, wodurch NullPointerExceptions und andere Fehler vermieden werden.