Dependency Injection ist ein Entwurfsmuster, das es ermöglicht, Objekte zu erstellen, die andere Objekte in ihrem Inneren verwenden (diese Definition werden wir weiter unten genauer untersuchen).
Fangen wir mit einem Beispiel an: Das Einspeisen in einen Service-Komponente ist ein Beispiel für die Verwendung von Dependency Injection.
Warum benötigen wir überhaupt diesen Dependency-Injection-Mechanismus? Welche Vorteile bietet er uns? Lassen Sie uns dies anhand von Beispielen genauer betrachten:
In diesem Code-Schnipsel haben wir eine Beziehung namens Komposition implementiert. Diese tritt auf, wenn eine Entität eine andere Entität in sich aufnimmt und deren Lebenszyklus vollständig verwaltet. Der Prozess ist ziemlich klar und unkompliziert. Wir haben einfach den Konstruktor der Klasse ElectricEngine gekennzeichnet, Speicher dafür zugewiesen und die Variable this.engine auf diese Speicherposition gelenkt. Nun können wir über die Referenz auf die Klasse ElectricEngine von der Klasse Car aus zugreifen.
Betrachten wir das nächste Beispiel:
Hier haben wir eine weitere Entität hinzugefügt. Jetzt wird der Konstruktor unserer Elektromotor-Klasse ein Objekt vom Typ Starter als Parameter entgegennehmen. Im Gegenzug muss die Klasse Car, beim Erstellen einer Instanz der Klasse ElectricEngine, new ElectricEngine(new Starter()) als Argument übergeben. Stellen Sie sich nun vor, ElectricEngine nicht nur eine Klasse in seinem Konstruktor akzeptiert, sondern mindestens 3-4. Dann wird der Aufruf des Konstruktors der Klasse ElectricEngine entsprechend zunehmen. Und dies ist noch nicht das Limit:
Der DI-Mechanismus ermöglicht es uns, uns keine Gedanken darüber machen zu müssen, welche Entitäten ElectricEngine intern verwendet.
Lassen Sie uns nun zu Diensten und Komponenten zurückkehren. Stellen Sie sich eine Situation vor, in der unser Service intern einen httpClient und zwei weitere Dienste verwendet. Wenn wir eine Instanz unseres Dienstes in einer Komponente erstellen möchten, müssten wir auch Instanzen von httpClient und den anderen beiden Diensten erstellen, was unsere Aufgabe erheblich kompliziert und die Code-Lesbarkeit verringert.
Injektor in Angular
Ein Injektor ist ein Bestandteil im Dependency-Injection (DI)-System, der nach einer Abhängigkeit anhand eines Schlüssels im Container sucht oder eine Abhängigkeit mithilfe einer Konfiguration aus dem Anbieter erstellt. Der Anbieter gibt an, wie Instanzen von Abhängigkeiten erstellt und bereitgestellt werden sollen.
Zunächst einmal ist es notwendig, die Hierarchie der Injektoren als eine Art Baum mit eigenen Konstruktionsregeln zu betrachten. Basierend auf diesem Schema können wir schlussfolgern, dass unser Baum aus folgenden Elementen besteht:
- Plattform-Injektor: Dies ist der globale Injektor, der dafür verantwortlich ist, Abhängigkeiten vor der Erstellung der ersten Komponenten und sogar der Module bereitzustellen. Dieser Injektor behandelt beispielsweise Dienste wie den Seitentitel. Es ist erwähnenswert, dass durch Markieren eines Dienstes mit providedIn: „platform“ der Dienst über alle Unteranwendungen in unserer Anwendung hinweg geteilt wird. (Wir können ein Hauptmodul oder mehrere anschließen und unsere Anwendung im Wesentlichen aus zwei oder mehr Anwendungen bestehen lassen).
- Wurzel-Injektor: Wenn wir providedIn: „root“ in einem Dienst spezifizieren, erstellen wir ein Singleton für die gesamte Anwendung (oder Unteranwendungen, wenn es mehr als ein Hauptmodul gibt). Was ist ein Singleton? In einfachen Worten handelt es sich um einen Dienst, der für die gesamte Anwendung, in der wir ihn bereitstellen, in einer einzigen Instanz existiert.
- Lazy-Route-Injektor / Modul-Injektor: Die Angular-DI-Struktur ist so konzipiert, dass jedes Modul seinen eigenen Injektor hat. Der Unterschied zwischen einem lazy geladenen Modul und einem regulären besteht darin, dass der Injektor des Lazy-Moduls Abhängigkeiten erst auflöst, nachdem das Modul in den Bundle geladen wurde.
- Node-Injektor: Dieser Injektor ist darauf ausgelegt, Abhängigkeiten innerhalb von Komponenten und Direktiven aufzulösen.
Lassen Sie uns nun in die Details eintauchen:
Wir werden damit beginnen zu verstehen, wann unser Dienst in der DI zu einem Singleton wird und wann er mehrere Instanzen haben kann.
1. Betrachten Sie eine Situation, in der wir ein AppModule haben, in dem wir einen UserService bereitgestellt haben. Dieser Dienst wird zu einem Singleton für andere Module, die wir im imports-Array einbeziehen. Mit anderen Worten, wenn wir den UserService in eine Komponente oder Direktive injizieren, wird er in einer einzigen Instanz sein. ABER! Wenn wir im Component/Module/Directive selbst das providers-Array nicht überschreiben, indem wir dort den UserService übergeben (dies werden wir weiter unten besprechen).
P.S. Überprüfen Sie die Konsolenausgabe, und wenn die Zahl, die vom UserService erhalten wird, dieselbe ist, ist unser Dienst ein Singleton.
Nun betrachten wir eine Situation, in der wir den UserService sowohl im providers-Array auf der AppModule-Ebene als auch auf der Ebene einer SomeTestComponent bereitgestellt haben. In diesem Fall wird der Dienst zu einem Singleton für alle Komponenten und Direktiven, die mit AppModule verknüpft sind, jedoch nicht für SomeTestComponent und (wichtig!) alle seine untergeordneten Komponenten.
Injektoren wie Plattform und Root sind global für die Anwendung/Anwendungen. Das bedeutet, dass durch einmaliges Bereitstellen des Dienstes eine einzige Instanz für die gesamte Anwendung entsteht, was sie zu einem Singleton macht. ABER! Vergessen wir nicht, dass auf der Ebene von 1. Modul 2. Komponente 3. Direktive der Dienst erneut bereitgestellt werden kann, was zur Erstellung einer anderen Instanz des Dienstes in der Anwendung führt.
Nun sprechen wir über Lazy-Module. Jedes von ihnen hat seinen eigenen Injektor, der nur aktiviert wird, wenn das Modul in das Bundle geladen wurde. Es „erbt“ jedoch Abhängigkeiten von seinem Hauptmodul, zu dem es importiert wurde. Wenn wir also einen UserService in AppModule bereitgestellt haben und es der Stamm für das Lazy-Loaded SitesModule ist, wird die Instanz von UserService in SitesModule die gleiche sein wie in AppModule. ABER! Es ist wichtig zu bedenken, dass wir den Dienst auf der Modul-, Komponenten- und Direktiven-Ebene bereitstellen können (siehe Punkt 2).
Als Nächstes betrachten wir ein Umsetzungsbeispiel. Lassen Sie uns einen Dienst erstellen:
Lassen Sie uns unseren Dienst im AppModule bereitstellen:
Als Nächstes müssen Sie SitesModule und SiteComponent erstellen, in denen wir sofort UserService injizieren werden.
Zu Testzwecken injizieren wir UserService auch in AppComponent:
Lazy Laden:
Und nun werden Sie feststellen, dass beim Navigieren durch die Route die Konsole die gleiche Zahl für sowohl die App- als auch die Benutzerkomponenten ausgibt.
Bereitstellungstypen
Aus dem vorherigen Artikel haben wir etwas über Injektoren gelernt; jetzt müssen wir verschiedene Situationen verstehen, die während der Bereitstellung auftreten können:
- Providers: [UserService] Der einfachste und vertrauteste Typ der Dienstbereitstellung in einem Modul. Wir geben einfach den Namen des Dienstes an, den wir auf der Ebene eines bestimmten Moduls in der Anwendung bereitstellen möchten. In diesem Fall betrachten wir das AppModule. Unser Dienst wird zu einem Singleton und steht für die Injektion in 1. Komponenten.
- Direktiven im Bereich des AppModule sowie in den in dieses importierten Modulen zur Verfügung. Diese Regeln gelten auch für Lazy-Module, für die AppModule als Wurzel dient.
- Providers: [{ provide: UserService, useClass: UserService }]
Ähnlich wie im Providers-Array können Sie anstelle des Dienstnamens ein Objekt übergeben. In der useClass-Eigenschaft geben wir explizit an, welcher Dienst bei der Injektion von UserService verwendet werden soll. Dies entspricht der Notation [UserService], aber unter der Haube passiert alles wie im Beispiel Nr. 2, was Angular-Entwicklern das Leben erleichtert.
2. Providers: [{ provide: UserService, useClass: NewUserService }]
Lassen Sie uns eine weitere Bereitstellungsoption aus Beispiel Nr. 2 betrachten. Wie zuvor erwähnt, übergeben wir in useClass den Dienst, der bei der Injektion von UserService verwendet wird. ABER! Auf diese Weise (Beispiel 3) können wir in den Modulen, in denen es benötigt wird, UserService durch NewUserService ersetzen, und die Klasse NewUserService wird verwendet. Wann kann dies nützlich sein? Zum Beispiel, wenn sich die Logik in einem bestimmten Teil der Anwendung geändert hat und speziell für diesen Teil ein neuer Dienst implementiert wurde. Dank der Möglichkeiten der Dependency Injection (DI) müssen wir nur eine Zeile im Modul ändern, anstatt den Dienst im Konstruktor jeder Komponente zu ersetzen.
- Providers: [{ provide: UserService, useValue: { name: ‚Some Name‘ }}]
Anstelle eines Dienstes an useClass vorbeizuleiten, bietet die DI die Möglichkeit, ein bestimmtes Objekt zu übergeben. Wenn wir also UserService injizieren, liefert uns die DI nicht eine Instanz des Dienstes, sondern das Objekt, das wir durch die Bereitstellung bereitgestellt haben.
4. Providers: [NewUserService, { provide: UserService, useExisting: NewUserService }]
Und jetzt registrieren wir NewUserService separat. Dabei, wenn wir die Option mit useClass: NewUserService beibehalten, werden wir feststellen, dass zwei Instanzen von NewUserService erstellt werden, zwei verschiedene Singleton-Instanzen. Um diese Situation zu vermeiden, stellt die DI eine Methode namens useExisting zur Verfügung, die sicherstellt, dass nur eine Instanz erstellt wird, nicht zwei. In einfachen Worten bitten wir die DI, keine neue Instanz der Klasse zu erstellen, sondern die vorhandene zu verwenden. Dies ist nützlich, wenn wir uns nicht sicher sind, dass durch die Bereitstellung des Dienstes im Modul eine einzelne Instanz entsteht.
- Providers: [{ provide: UserService, useFactory: (local: string): string | null {return….} ]
Eine andere Bereitstellungsoption ist die Verwendung von useFactory. Dies kann nützlich sein, wenn wir etwas Logik extrahieren möchten, die „on-the-fly“ berechnet wird, und wir können auch einige Bedingungen hinzufügen. Anstatt einen ganzen Dienst zu erstellen, können wir durch die DI ein spezifisches Stück Logik bereitstellen (DATE_AND_TIME_FORMAT — InjectionToken, den wir später behandeln werden):
Es ist erwähnenswert, dass wir in deps: [LOCALE_ID] ein Objekt übergeben, aus dem Werte an den Parameter useFactory: (local: string) übergeben werden. ABER! Nichts hindert uns daran, Instanzen von Diensten in useFactory zurückzugeben! Wir können eine ähnliche Situation simulieren:
Abschließend können wir sagen, dass useFactory verwendet werden kann, wenn wir etwas ‚on-the-fly‘ berechnen oder basierend auf bestimmten Bedingungen.
6. InjectionToken<type>(Beschreibung);
Und schließlich die letzte Bereitstellungsoption wird verwendet, wenn wir eine Entität bereitstellen möchten, die kein Dienst ist. Es funktioniert gut in Kombination mit useFactory. Lassen Sie uns nun das Bild aus dem vorherigen Beispiel vervollständigen, indem wir einen Ordner erstellen und ihn „config“ nennen. Darin erklären wir zwei Konstanten unter Verwendung von InjectionToken:
In dieser Situation wird LOCALE_ID im Array deps: [LOCALE_ID] für unseren Anbieter Werte an useFactory(locale: string) bereitstellen. Wenn wir DATE_AND_TIME_FORMAT bereitstellen, erhalten wir Datumsoptionen, abhängig davon, was im lokalen Speicher gespeichert ist, das von LOCALE_ID erhalten wurde.
Zusammenfassung
Dependency Injection (DI) ist eine Programmiermethode, die es Objekten in einer Anwendung ermöglicht, locker gekoppelt, wiederverwendbar, leicht refaktorisierbar und testbar zu sein. DI erweist sich als wertvoll bei der Entwicklung verschiedener Lösungen und Produkte, insbesondere solcher mit komplexen Architekturen, mehrschichtiger Logik, unterschiedlichen Implementierungen einer einzelnen Schnittstelle, externen Abhängigkeiten oder der Notwendigkeit, sich an sich ändernde Anforderungen anzupassen.
DI kann dazu beitragen, Probleme wie:
• Enge Kopplung zwischen Klassen, die den Austausch, die Erweiterung oder die Wiederverwendung herausfordernd macht.
• Verletzung des Single Responsibility Principle, bei dem eine Klasse autonom ihre Abhängigkeiten erstellt und verwaltet, anstatt diese Aufgabe an einen externen Mechanismus zu delegieren.
• Komplexität beim Testen von Klassen mit versteckten oder fest codierten Abhängigkeiten, die nicht isoliert oder ausgetauscht werden können.
• Geringe Modularität und Flexibilität in einer Anwendung, die sich nicht an verschiedene Nutzungsszenarien, Konfigurationen oder Plattformen anpassen kann.
Zusammenfassend ist DI eine nützliche und leistungsstarke Programmiermethode, die die Qualität, Leistung und Skalierbarkeit von Anwendungen verbessern kann.
Lesen Sie weitere Artikel von unseren Experten:
- Erstellung einer Spring Boot-Anwendung mit Apache Solr für Suchfunktionen
- Wie man den Support-Chatbot in nur 3 Schritten mit OpenAI API erstellt