
In diesem Artikel werde ich in einer einfachen Sprache über die Grundlagen der Abhängigkeitsinjektion (Eng. Dependency Injection, DI ) und auch über die Gründe für die Verwendung dieses Ansatzes sprechen. Dieser Artikel richtet sich an Personen, die nicht wissen, was Abhängigkeitsinjektion ist, oder die Zweifel an der Notwendigkeit dieser Technik haben. Also fangen wir an.
Was ist Sucht?
Schauen wir uns zuerst ein Beispiel an. Wir haben ClassA
, ClassB
und ClassC
wie unten gezeigt:
class ClassA { var classB: ClassB } class ClassB { var classC: ClassC } class ClassC { }
Sie können sehen, dass die ClassA
Klasse eine Instanz der ClassA
Klasse enthält. Wir können also sagen, dass die ClassA
Klasse von der ClassA
Klasse abhängt. Warum? Weil ClassA
benötigt, um korrekt zu funktionieren. Wir können auch sagen, dass die ClassB
Klasse eine Abhängigkeit von der ClassA
Klasse ist.
Bevor ich fortfahre, möchte ich klarstellen, dass eine solche Beziehung gut ist, da wir nicht eine Klasse benötigen, um die gesamte Arbeit in der Anwendung zu erledigen. Wir müssen die Logik in verschiedene Klassen unterteilen, von denen jede für eine bestimmte Funktion verantwortlich ist. In diesem Fall können die Klassen effektiv interagieren.
Wie arbeite ich mit Abhängigkeiten?
Schauen wir uns drei Methoden an, mit denen Abhängigkeitsinjektionsaufgaben ausgeführt werden:
Erster Weg: Erstellen Sie Abhängigkeiten in einer abhängigen Klasse
Einfach ausgedrückt, wir können Objekte erstellen, wann immer wir sie benötigen. Schauen Sie sich das folgende Beispiel an:
class ClassA { var classB: ClassB fun someMethodOrConstructor() { classB = ClassB() classB.doSomething() } }
Es ist sehr einfach! Wir erstellen eine Klasse, wenn wir sie brauchen.
Die Vorteile
- Es ist leicht und einfach.
- Die abhängige Klasse (in unserem Fall
ClassA
) steuert vollständig, wie und wann die Abhängigkeiten erstellt werden sollen.
Nachteile
ClassA
und ClassA
ClassB
eng miteinander verwandt. Wenn wir ClassA
verwenden ClassA
, müssen wir daher ClassA
verwenden, und es ist unmöglich, ClassB
durch etwas anderes zu ersetzen .- Bei jeder Änderung der Initialisierung der
ClassB
Klasse müssen Sie den Code innerhalb der ClassA
Klasse (und aller anderen von ClassA
abhängigen Klassen) anpassen. Dies erschwert den Prozess des Änderns der Abhängigkeit. ClassA
kann nicht getestet werden. Wenn Sie eine Klasse testen müssen und dies dennoch einer der wichtigsten Aspekte der Softwareentwicklung ist, müssen Sie die Unit-Tests für jede Klasse separat durchführen. Dies bedeutet, dass Sie, wenn Sie ClassA
die korrekte Funktionsweise der ClassA
Klasse überprüfen und mehrere ClassA
erstellen möchten, wie im Beispiel gezeigt, auf jeden Fall eine Instanz der ClassB
Klasse erstellen, auch wenn dies für Sie nicht von Interesse ist. Wenn beim Testen ein Fehler auftritt, können Sie nicht verstehen, wo er sich befindet - in ClassA
oder ClassA
Schließlich besteht die Möglichkeit, dass ein Teil des Codes in ClassB
zu einem Fehler geführt hat, während ClassA
ordnungsgemäß funktioniert. Mit anderen Worten, Unit-Tests sind nicht möglich, da Module (Klassen) nicht voneinander getrennt werden können.ClassA
muss so konfiguriert sein, dass Abhängigkeiten ClassA
werden können. In unserem Beispiel muss er wissen, wie man eine ClassC
und daraus eine ClassC
erstellt. Es wäre besser, wenn er nichts davon wüsste. Warum? Aufgrund des Prinzips der Einzelverantwortung .
Jede Klasse sollte nur ihren Job machen.
Daher möchten wir nicht, dass Klassen für etwas anderes als ihre eigenen Aufgaben verantwortlich sind. Die Implementierung von Abhängigkeiten ist eine zusätzliche Aufgabe, die wir für sie festlegen.
Zweiter Weg: Abhängigkeiten über eine benutzerdefinierte Klasse einfügen
Um zu verstehen, dass das Einfügen von Abhängigkeiten in eine abhängige Klasse keine gute Idee ist, wollen wir einen alternativen Weg untersuchen. Hier definiert die abhängige Klasse alle Abhängigkeiten, die sie im Konstruktor benötigt, und ermöglicht es der Benutzerklasse, sie bereitzustellen. Ist das eine Lösung für unser Problem? Wir werden es etwas später herausfinden.
Schauen Sie sich den folgenden Beispielcode an:
class ClassA { var classB: ClassB constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC constructor(classC: ClassC){ this.classC = classC } } class ClassC { constructor(){ } } class UserClass(){ fun doSomething(){ val classC = ClassC(); val classB = ClassB(classC); val classA = ClassA(classB); classA.someMethod(); } } view rawDI Example In Medium -
Jetzt erhält ClassA
alle Abhängigkeiten im Konstruktor und kann einfach die Methoden der ClassB
Klasse aufrufen, ohne etwas zu initialisieren.
Die Vorteile
ClassA
und ClassB
jetzt lose miteinander verbunden, und wir können ClassA
ersetzen, ohne den Code in ClassA
. Anstatt beispielsweise AssumeClassB
, können wir AssumeClassB
, eine Unterklasse von ClassB
, und unser Programm funktioniert ordnungsgemäß.ClassA
kann jetzt getestet werden. Beim Schreiben eines ClassA
können wir eine eigene Version von ClassB
(Testobjekt) erstellen und an ClassA
. Wenn beim Bestehen des Tests ein Fehler auftritt, wissen wir jetzt mit Sicherheit, dass dies definitiv ein Fehler in ClassA
.ClassB
von der Arbeit mit Abhängigkeiten befreit und kann sich auf seine Aufgaben konzentrieren.
Nachteile
- Diese Methode ähnelt einem Kettenmechanismus, und irgendwann sollte die Kette unterbrochen werden. Mit anderen Worten, der Benutzer der Klasse
ClassA
muss alles über die Initialisierung von ClassA
wissen, was wiederum Kenntnisse über die Initialisierung von ClassC
usw. erfordert. Sie sehen also, dass jede Änderung im Konstruktor einer dieser Klassen zu einer Änderung in der aufrufenden Klasse führen kann, ganz zu schweigen davon, dass ClassA
mehr als einen Benutzer haben kann, sodass die Logik zum Erstellen von Objekten wiederholt wird. - Trotz der Tatsache, dass unsere Abhängigkeiten klar und leicht zu verstehen sind, ist Benutzercode nicht trivial und schwer zu verwalten. Daher ist nicht alles so einfach. Darüber hinaus verstößt der Code gegen das Prinzip der Einzelverantwortung, da er nicht nur für seine Arbeit, sondern auch für die Implementierung von Abhängigkeiten in abhängigen Klassen verantwortlich ist.
Die zweite Methode funktioniert offensichtlich besser als die erste, hat aber immer noch ihre Mängel. Ist es möglich, eine geeignetere Lösung zu finden? Bevor wir den dritten Weg betrachten, wollen wir zunächst über das Konzept der Abhängigkeitsinjektion sprechen.
Was ist Abhängigkeitsinjektion?
Die Abhängigkeitsinjektion ist eine Möglichkeit, Abhängigkeiten außerhalb der abhängigen Klasse zu behandeln, wenn die abhängige Klasse nichts tun muss.
Basierend auf dieser Definition verwendet unsere erste Lösung offensichtlich nicht die Idee der Abhängigkeitsinjektion, und der zweite Weg besteht darin, dass die abhängige Klasse nichts unternimmt, um die Abhängigkeiten bereitzustellen. Aber wir denken immer noch, dass die zweite Lösung schlecht ist. WARUM ?!
Da die Definition der Abhängigkeitsinjektion nichts darüber aussagt, wo die Arbeit mit Abhängigkeiten stattfinden soll (außer außerhalb der abhängigen Klasse), muss der Entwickler einen geeigneten Ort für die Abhängigkeitsinjektion auswählen. Wie Sie dem zweiten Beispiel entnehmen können, ist die Benutzerklasse nicht ganz der richtige Ort.
Wie kann man es besser machen? Schauen wir uns einen dritten Weg an, um mit Abhängigkeiten umzugehen.
Dritter Weg: Lassen Sie jemanden anstelle von uns mit Abhängigkeiten umgehen
Nach dem ersten Ansatz sind abhängige Klassen dafür verantwortlich, ihre eigenen Abhängigkeiten zu erhalten, und nach dem zweiten Ansatz haben wir die Verarbeitung von Abhängigkeiten von der abhängigen Klasse in die Benutzerklasse verschoben. Stellen wir uns vor, es gibt jemanden, der mit den Abhängigkeiten umgehen kann, wodurch weder die abhängigen noch die Benutzerklassen den Job erledigen würden. Mit dieser Methode können Sie direkt mit Abhängigkeiten in der Anwendung arbeiten.
Eine "saubere" Implementierung der Abhängigkeitsinjektion (meiner persönlichen Meinung nach)
Die Verantwortung für den Umgang mit Abhängigkeiten liegt bei einem Dritten, sodass kein Teil der Anwendung mit diesen interagiert.
Die Abhängigkeitsinjektion ist keine Technologie, kein Framework, keine Bibliothek oder ähnliches. Dies ist nur eine Idee. Die Idee ist, mit Abhängigkeiten außerhalb der abhängigen Klasse zu arbeiten (vorzugsweise in einem speziell zugewiesenen Teil). Sie können diese Idee anwenden, ohne Bibliotheken oder Frameworks zu verwenden. Normalerweise wenden wir uns jedoch Frameworks zur Implementierung von Abhängigkeiten zu, da dies die Arbeit vereinfacht und das Schreiben von Vorlagencode vermeidet.
Jedes Abhängigkeitsinjektions-Framework weist zwei inhärente Merkmale auf. Möglicherweise stehen Ihnen weitere Zusatzfunktionen zur Verfügung, diese beiden Funktionen sind jedoch immer vorhanden:
Erstens bieten diese Frameworks eine Möglichkeit, die Felder (Objekte) zu bestimmen, die implementiert werden sollen. Einige Frameworks tun dies, indem sie ein Feld oder einen Konstruktor mit der Annotation @Inject
Anmerkungen versehen. Es gibt jedoch auch andere Methoden. Beispielsweise verwendet Koin die in Kotlin integrierten Sprachfunktionen, um die Implementierung zu bestimmen. Inject
bedeutet, dass die Abhängigkeit vom DI-Framework behandelt werden muss. Der Code sieht ungefähr so aus:
class ClassA { var classB: ClassB @Inject constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC @Inject constructor(classC: ClassC){ this.classC = classC } } class ClassC { @Inject constructor(){ } }
Zweitens können Sie mithilfe von Frameworks festlegen, wie die einzelnen Abhängigkeiten bereitgestellt werden sollen. Dies geschieht in separaten Dateien. Ungefähr so sieht es aus (denken Sie daran, dass dies nur ein Beispiel ist und von Framework zu Framework unterschiedlich sein kann):
class OurThirdPartyGuy { fun provideClassC(){ return ClassC()
Wie Sie sehen, ist jede Funktion für die Verarbeitung einer Abhängigkeit verantwortlich. Wenn wir ClassA
irgendwo in der Anwendung ClassA
müssen, ClassA
Folgendes: Unser DI-Framework erstellt eine Instanz der ClassC
Klasse, indem es provideClassB
provideClassC
, an provideClassB
und eine Instanz von provideClassB
empfängt, die an provideClassA
wird. Als Ergebnis wird ClassA
erstellt. Das ist fast magisch. Lassen Sie uns nun die Vor- und Nachteile der dritten Methode untersuchen.
Die Vorteile
- Alles ist so einfach wie möglich. Sowohl die abhängige Klasse als auch die Klasse, die die Abhängigkeiten bereitstellt, sind klar und einfach.
- Klassen sind lose gekoppelt und können leicht durch andere Klassen ersetzt werden. Angenommen, wir möchten
ClassC
durch AssumeClassC
ersetzen, eine Unterklasse von ClassC
. Dazu müssen Sie nur den Provider-Code wie folgt ändern. Wo immer ClassC
verwendet wird, wird die neue Version jetzt automatisch verwendet:
fun provideClassC(){ return AssumeClassC() }
Bitte beachten Sie, dass sich kein Code in der Anwendung ändert, sondern nur die Anbietermethode. Es scheint, dass nichts noch einfacher und flexibler sein könnte.
- Unglaubliche Testbarkeit. Sie können Abhängigkeiten während des Tests problemlos durch Testversionen ersetzen. In der Tat ist die Abhängigkeitsinjektion Ihr Haupthelfer beim Testen.
- Verbesserung der Codestruktur, as Die Anwendung verfügt über einen separaten Ort für die Abhängigkeitsverarbeitung. Infolgedessen kann sich der Rest der Anwendung ausschließlich auf ihre Funktionen konzentrieren und darf sich nicht mit Abhängigkeiten überschneiden.
Nachteile
- DI-Frameworks haben eine bestimmte Einstiegsschwelle, daher muss das Projektteam Zeit aufwenden und diese studieren, bevor sie effektiv eingesetzt werden können.
Fazit
- Abhängigkeitsbehandlung ohne DI ist möglich, kann jedoch zu Anwendungsabstürzen führen.
- DI ist nur eine effektive Idee, nach der es möglich ist, Abhängigkeiten außerhalb der abhängigen Klasse zu behandeln.
- Es ist am effektivsten, DI in bestimmten Teilen der Anwendung zu verwenden. Viele Frameworks tragen dazu bei.
- Frameworks und Bibliotheken werden für DI nicht benötigt, können aber sehr hilfreich sein.
In diesem Artikel habe ich versucht, die Grundlagen der Arbeit mit dem Konzept der Abhängigkeitsinjektion zu erläutern, und auch die Gründe für die Verwendung dieser Idee aufgelistet. Es gibt viele weitere Ressourcen, die Sie erkunden können, um mehr über die Verwendung von DI in Ihren eigenen Anwendungen zu erfahren. Ein separater Abschnitt im fortgeschrittenen Teil unseres Android-Berufskurses ist diesem Thema gewidmet.