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:
Zuerst richten wir TSyringe in Ihrem TypeScript-Projekt ein:
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 TS-Spritzenzucker 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? Damit soll TSyringe mitgeteilt werden, 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, zur Laufzeit die Instanz oder den Wert, den sie hat, für das Token UserAdapter für die userAdapter-Eigenschaft 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); ... } }
Zu diesem Zeitpunkt 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 Dependency-Injection-Registrierung 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 }
Aber wir können 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>("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", userAdapter) userCommands = container.resolve<UserCommands>("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");
Scoped containers: Use scoped containers for request-scoped dependencies in web applications.
Don't overuse DI: Not everything needs to be injected. Use DI for cross-cutting concerns and configurable dependencies.
If you've come this far, I want to say thank you for reading. I hope you found this article instructive. Remember to always consider the specific needs of your project when implementing dependency injection and architectural patterns.
Likes and comment feedback are the best ways to improve.
Happy coding!
Das obige ist der detaillierte Inhalt vonTSyringe und Abhängigkeitsinjektion in TypeScript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!