Ich bin kein großer Fan von großen Frameworks wie NestJS; Mir gefiel schon immer die Freiheit, meine Software so zu erstellen, wie ich es möchte, mit einer Struktur, die ich auf einfache Weise entscheide. Aber etwas, das mir beim Testen von NestJS gefallen hat, war die Abhängigkeitsinjektion.
Dependency Injection (DI) ist ein Entwurfsmuster, das es uns ermöglicht, lose gekoppelten Code zu entwickeln, indem wir die Verantwortung für die Erstellung und Verwaltung von Abhängigkeiten aus unseren Klassen entfernen. Dieses Muster ist entscheidend für das Schreiben wartbarer, testbarer und skalierbarer Anwendungen. Im TypeScript-Ökosystem zeichnet sich TSyringe als leistungsstarker und leichter Dependency-Injection-Container aus, der diesen Prozess vereinfacht.
TSyringe ist ein leichter Abhängigkeitsinjektionscontainer für TypeScript/JavaScript-Anwendungen. Es wird von Microsoft auf GitHub (https://github.com/microsoft/tsyringe) verwaltet und verwendet Dekoratoren für die Konstruktor-Injection. Anschließend wird ein Inversion of Control-Container verwendet, um die Abhängigkeiten basierend auf einem Token zu speichern, das Sie gegen eine Instanz oder einen Wert austauschen können.
Bevor wir uns mit TSyringe befassen, wollen wir kurz untersuchen, was Abhängigkeitsinjektion ist und warum sie wichtig ist.
Abhängigkeitsinjektion ist eine Technik, bei der ein Objekt seine Abhängigkeiten von externen Quellen erhält, anstatt sie selbst zu erstellen. Dieser Ansatz bietet mehrere Vorteile:
Lassen Sie uns zunächst TSyringe in Ihrem TypeScript-Projekt einrichten:
npm install tsyringe reflect-metadata
Stellen Sie sicher, dass Sie in Ihrer tsconfig.json die folgenden Optionen haben:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }
Reflect-Metadaten am Einstiegspunkt Ihrer Anwendung importieren:
import "reflect-metadata";
Der Einstiegspunkt Ihrer Anwendung ist beispielsweise das Root-Layout auf Next.js 13 oder es kann die Hauptdatei in einer kleinen Express-Anwendung sein.
Nehmen wir das Beispiel aus der Einleitung und fügen den TSyringe-Zucker hinzu:
Beginnen wir mit dem Adapter.
// @/adapters/userAdapter.ts import { injectable } from "tsyringe" @injectable() class UserAdapter { constructor(...) {...} async fetchByUUID(uuid) {...} }
Beachten Sie den @injectable()-Decorator? Es soll TSyringe mitteilen, dass diese Klasse zur Laufzeit injiziert werden kann.
Mein Dienst verwendet also den Adapter, den wir gerade erstellt haben. Lassen Sie uns diesen Adapter in meinen Dienst einbinden.
// @/core/user/user.service.ts import { injectable, inject } from "tsyringe" ... @injectable() class UserService { constructor(@inject('UserAdapter') private readonly userAdapter: UserAdapter) {} async fetchByUUID(uuid: string) { ... const { data, error } = await this.userAdapter.fetchByUUID(uuid); ... } }
Hier habe ich auch den @injectable-Dekorator verwendet, da der Dienst in meine Befehlsklasse eingefügt werden soll, aber ich habe auch den @inject-Dekorator in den Konstruktorparametern hinzugefügt. Dieser Dekorator weist TSyringe an, die Instanz oder den Wert, den sie hat, für das Token UserAdapter für die userAdapter-Eigenschaft zur Laufzeit anzugeben.
Und zu guter Letzt die Wurzel meines Kerns: die Befehlsklasse (oft fälschlicherweise Usecase genannt).
// @/core/user/user.commands.ts import { inject } from "tsyringe" ... @injectable() class UserCommands { constructor(@inject('UserService') private readonly userService: UserService) {} async fetchByUUID(uuid) { ... const { data, error } = this.userService.fetchByUUID(uuid); ... } }
An diesem Punkt haben wir TSyringe mitgeteilt, was injiziert werden soll und was im Konstruktor injiziert werden soll. Aber wir haben unsere Container noch nicht zum Speichern der Abhängigkeiten erstellt. Wir können das auf zwei Arten tun:
Wir können eine Datei mit unserer Abhängigkeitsinjektionsregistrierung erstellen:
// @/core/user/user.dependencies.ts import { container } from "tsyringe" ... container.register("UserService", {useClass: UserService}) // associate the UserService with the token "UserService" container.register("UserAdapter", {useClass: UserAdapter}) // associate the UserAdapter with the token "UserAdapter" export { container }
Wir können aber auch den @registry-Decorator verwenden.
// @/core/user/user.commands.ts import { inject, registry, injectable } from "tsyringe" ... @injectable() @registry([ { token: 'UserService', useClass: UserService }, { token: 'UserAdapter', useClass: UserAdapter }, ]) export class UserCommands { constructor(@inject('UserService') private readonly userService: UserService) {} async fetchByUUID(uuid) { ... const { data, error } = this.userService.fetchByUUID(uuid); ... } } container.register("UserCommands", { useClass: UserCommands}) export { container }
Beide Methoden haben Vor- und Nachteile, aber letztendlich ist es Geschmackssache.
Da unser Container nun mit unseren Abhängigkeiten gefüllt ist, können wir sie nach Bedarf aus dem Container abrufen, indem wir die Auflösungsmethode des Containers verwenden.
import { container, UserCommands } from "@/core/user/user.commands" ... const userCommands = container.resolve("UserCommands") await userCommands.fetchByUUID(uuid) ...
Dieses Beispiel ist ziemlich einfach, da jede Klasse nur von einer anderen abhängt, unsere Dienste jedoch von vielen abhängen könnten und die Abhängigkeitsinjektion wirklich dazu beitragen würde, alles aufgeräumt zu halten.
Aber warte! Lass mich nicht so zurück! Wie wäre es mit den Tests?
Unsere Injektionen können uns auch beim Testen unseres Codes helfen, indem sie Scheinobjekte direkt in unsere Abhängigkeiten senden. Sehen wir uns ein Codebeispiel an:
import { container, UserCommands } from "@/core/user/user.commands" describe("test ftw", () => { let userAdapterMock: UserAdapterMock let userCommands: UserCommands beforeEach(() => { userAdapterMock = new UserAdapter() container.registerInstance("UserAdapter", userAdapter) userCommands = container.resolve ("UserCommands") }); ... });
Jetzt enthält das UserAdapter-Token einen Schein, der in die abhängigen Klassen eingefügt wird.
Token zur Benennung verwenden: Anstatt String-Literale für Injektionstoken zu verwenden, erstellen Sie konstante Token:
export const USER_REPOSITORY_TOKEN = Symbol("UserRepository");
Bereichsbezogene Container: Verwenden Sie Bereichscontainer für anforderungsbezogene Abhängigkeiten in Webanwendungen.
DI nicht überbeanspruchen: Nicht alles muss injiziert werden. Nutzen Sie DI für Querschnittsthemen und konfigurierbare Abhängigkeiten.
Wenn Sie bis hierher gekommen sind, möchte ich mich für das Lesen bedanken. Ich hoffe, Sie fanden diesen Artikel lehrreich. Denken Sie daran, bei der Implementierung von Abhängigkeitsinjektion und Architekturmustern immer die spezifischen Anforderungen Ihres Projekts zu berücksichtigen.
Likes und Kommentar-Feedback sind die besten Möglichkeiten, sich zu verbessern.
Viel Spaß beim Codieren!
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3