Grundlagen der Abhängigkeitsinjektion

Grundlagen der Wahrnehmungsinjektion


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() //just creating an instance of the object and return it. } fun provideClassB(classC: ClassC){ return ClassB(classC) } fun provideClassA(classB: ClassB){ return ClassA(classB) } } 

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.

Source: https://habr.com/ru/post/de434380/


All Articles