Meistern von IoC in TypeScript mit InversifyJS oder TSyringe
James Reed
Infrastructure Engineer · Leapcell

Einleitung
In der komplexen Welt der modernen Softwareentwicklung hängt der Aufbau robuster und wartbarer Anwendungen oft davon ab, wie effektiv wir Abhängigkeiten zwischen verschiedenen Teilen unserer Codebasis verwalten. Da TypeScript aufgrund seiner starken Typisierung und Skalierbarkeit immer mehr an Bedeutung gewinnt, wird es unerlässlich, dafür zu sorgen, dass unsere Anwendungen lose gekoppelt und leicht testbar bleiben. Hier glänzt das Konzept der Inversion of Control (IoC). IoC, insbesondere wenn es über Dependency Injection (DI)-Frameworks implementiert wird, ermöglicht es uns, Komponenten zu entkoppeln, indem wir einer externen Entität die Verwaltung ihrer Abhängigkeiten überlassen, anstatt dass Komponenten ihre eigenen erstellen. Dieser Paradigmenwechsel verbessert die Modularität erheblich, erleichtert das Unit-Testing und führt letztendlich zu widerstandsfähigerer und anpassungsfähigerer Software. Im TypeScript-Ökosystem stechen InversifyJS und TSyringe als leistungsstarke Werkzeuge zur Erreichung dieses Ziels hervor und bieten elegante Lösungen für die Implementierung von IoC. In diesem Artikel werden wir die Kernprinzipien von IoC untersuchen und wie diese beiden Frameworks Entwickler befähigen, bessere TypeScript-Anwendungen zu erstellen.
Verständnis von Inversion of Control und Dependency Injection
Bevor wir uns mit den Besonderheiten von InversifyJS und TSyringe befassen, wollen wir ein klares Verständnis der grundlegenden Konzepte schaffen, die sie nutzen.
Inversion of Control (IoC) ist ein Designprinzip, bei dem der Kontrollfluss eines Programms umgekehrt wird. Anstatt dass die Anwendung wiederverwendbare Bibliotheken aufruft, ruft das Framework die Komponenten der Anwendung auf. Stellen Sie es sich als das Hollywood-Prinzip vor: "Ruf uns nicht an, wir rufen dich an." Im Kontext der Komponenteninteraktion bedeutet dies, dass eine Komponente ihre Abhängigkeiten nicht erstellt oder verwaltet; stattdessen stellt ein externer Mechanismus diese bereit.
Dependency Injection (DI) ist eine spezifische Implementierung von IoC. Es ist eine Technik, bei der ein Objekt andere Objekte erhält, von denen es abhängt. Diese "Abhängigkeiten" werden zur Laufzeit in das abhängige Objekt injiziert, anstatt dass das Objekt sie selbst erstellt oder sie aus einem Service-Locator abruft. Es gibt mehrere Möglichkeiten, wie Abhängigkeiten injiziert werden können:
- Constructor Injection: Abhängigkeiten werden über den Klassenkonstruktor bereitgestellt. Dies wird oft bevorzugt, da es sicherstellt, dass das Objekt in einem gültigen Zustand mit allen erforderlichen Abhängigkeiten erstellt wird.
- Setter Injection: Abhängigkeiten werden über öffentliche Setter-Methoden bereitgestellt. Dies ermöglicht optionale Abhängigkeiten oder die Änderung von Abhängigkeiten nach der Objekterstellung.
- Interface Injection: Abhängigkeiten werden über ein Interface bereitgestellt, das die Client-Klasse implementiert. (In TypeScript weniger verbreitet als in anderen Sprachen).
Die Vorteile von DI sind beträchtlich:
- Lose Kopplung: Komponenten sind nicht eng an ihre Implementierungen gebunden, was ihre Änderung oder ihren Austausch erleichtert.
- Testbarkeit: Das Mocking von Abhängigkeiten für Unit-Tests wird einfach, da Sie Test-Doubles anstelle von echten Implementierungen injizieren können.
- Wartbarkeit: Der Code wird aufgrund klarer Abhängigkeitsbeziehungen leichter verständlich und wartbar.
- Wiederverwendbarkeit: Komponenten werden in verschiedenen Kontexten wiederverwendbarer, da sie ihre Abhängigkeiten nicht fest codieren.
Mit diesen Konzepten im Hinterkopf wollen wir nun untersuchen, wie InversifyJS und TSyringe uns bei der Implementierung dieser Prinzipien in TypeScript unterstützen.
InversifyJS Deep Dive
InversifyJS ist ein leistungsstarker und sehr beliebter IoC-Container für TypeScript- und JavaScript-Anwendungen. Es nutzt Decorators und Typmetadaten zur Definition und Injektion von Abhängigkeiten.
Kernprinzipien und Setup
InversifyJS arbeitet mit drei Hauptkonzepten:
- Interfaces/Types: Definieren Sie die Verträge für Ihre Dienste. Dies fördert starke Typisierung und lose Kopplung.
- Klassen (Implementierungen): Stellen Sie die konkreten Implementierungen dieser Interfaces bereit.
- Container: Hier binden Sie Interfaces an ihre konkreten Implementierungen und lösen Instanzen auf.
Installation:
npm install inversify reflect-metadata npm install @types/reflect-metadata --save-dev
Sie müssen auch emitDecoratorMetadata
und experimentalDecorators
in Ihrer tsconfig.json
aktivieren:
{ "compilerOptions": { "target": "es6", "module": "commonjs", "lib": ["es6"], "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, "outDir": "./dist" } }
Und importieren Sie reflect-metadata
einmal am Einstiegspunkt Ihrer Anwendung:
import "reflect-metadata";
Beispielimplementierung
Stellen wir uns vor, wir erstellen eine einfache Anwendung, die einen Logging-Dienst benötigt.
1. Definieren eines Interfaces/Typs:
Es ist üblich in InversifyJS, eine Symbol
- oder string
-Konstante als Identifikator für Ihr Interface neben dem Interface selbst zu definieren.
// interfaces.ts export interface ILogger { log(message: string): void; } export const TYPES = { Logger: Symbol.for("Logger"), };
2. Implementieren des Dienstes:
// services.ts import { injectable } from "inversify"; import { ILogger } from "./interfaces"; @injectable() export class ConsoleLogger implements ILogger { public log(message: string): void { console.log(`[ConsoleLogger] ${message}`); } } @injectable() export class FileLogger implements ILogger { public log(message: string): void { console.log(`[FileLogger] Saving to file: ${message}`); // In einer realen App würde dies in eine Datei schreiben } }
Beachten Sie den @injectable()
-Decorator. Er markiert eine Klasse als Kandidaten für die Injektion durch InversifyJS.
3. Definieren eines Dienstes, der den Logger verwendet:
// app.ts import { injectable, inject } from "inversify"; import { ILogger, TYPES } from "./interfaces"; @injectable() export class Application { private _logger: ILogger; constructor(@inject(TYPES.Logger) logger: ILogger) { this._logger = logger; } public run(): void { this._logger.log("Application started!"); } }
Hier teilt @inject(TYPES.Logger)
InversifyJS mit, eine Instanz von ILogger
in den Konstruktor zu injizieren.
4. Konfigurieren des IoC-Containers:
// container.ts import { Container } from "inversify"; import { TYPES, ILogger } from "./interfaces"; import { ConsoleLogger, FileLogger } from "./services"; import { Application } from "./app"; const container = new Container(); // Binden Sie die Logger-Schnittstelle an ihre Implementierung container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger); // Oder .to(FileLogger) // Binden Sie die Application-Klasse container.bind<Application>(Application).toSelf(); // Bindet die Klasse an sich selbst export default container;
Hier bewirkt container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger)
die Magie. Es teilt dem Container mit: "Immer wenn jemand nach TYPES.Logger
fragt, stellen Sie eine Instanz von ConsoleLogger
bereit."
5. Auflösen und Ausführen:
// main.ts import container from "./container"; import { Application } from "./app"; // Holen Sie sich eine Instanz der Application vom Container const app = container.get<Application>(Application); app.run(); // Ausgabe: [ConsoleLogger] Application started!
Scopes
InversifyJS unterstützt verschiedene Bindungsumfänge:
.toConstantValue(value)
: Gibt immer denselben vordefinierten Wert zurück..toDynamicValue(factory)
: Gibt einen Wert zurück, der von einer Factory-Funktion produziert wird..toSelf()
: Bindet eine Klasse an sich selbst (nützlich für Klassen, die sowohl Dienste als auch Implementierungen sind)..inSingletonScope()
: Gibt bei jeder Anforderung dieselbe Instanz zurück..inTransientScope()
(Standard): Gibt bei jeder Anforderung eine neue Instanz zurück.
Beispiel für Singleton:
container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger).inSingletonScope();
Wenn Sie ConsoleLogger
in container.ts
durch FileLogger
ersetzen, ändert sich das Logging-Verhalten, ohne Application
oder main.ts
zu ändern, was die Leistungsfähigkeit von IoC demonstriert.
TSyringe Deep Dive
TSyringe ist ein leichtgewichtiger Dependency Injection Container für TypeScript und JavaScript, der eine einfache Möglichkeit zur Verwaltung und Injektion von Abhängigkeiten bietet. Er wird von Microsoft entwickelt und wird oft als eine leichtere Alternative zu InversifyJS angesehen, die die experimentellen Decorators und Reflect-Metadaten von TypeScript nutzt.
Kernprinzipien und Setup
TSyringe folgt einem sehr ähnlichen Muster wie InversifyJS und konzentriert sich auf das Dekorieren von Klassen und die Verwendung eines globalen Containers (oder spezifischer Container, falls bevorzugt).
Installation:
npm install tsyringe reflect-metadata npm install @types/reflect-metadata --save-dev
Ähnlich wie InversifyJS benötigen Sie emitDecoratorMetadata
und experimentalDecorators
in Ihrer tsconfig.json
und müssen reflect-metadata
einmal am Einstiegspunkt Ihrer Anwendung importieren.
{ "compilerOptions": { "target": "es6", "module": "commonjs", "lib": ["es6"], "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, // Empfohlen für Debugging "outDir": "./dist" } }
Und importieren Sie reflect-metadata
:
import "reflect-metadata";
Beispielimplementierung
Lassen Sie uns das Logger-Beispiel mit TSyringe neu implementieren.
1. Definieren eines Interfaces/Tokens:
Im Gegensatz zu InversifyJS, wo oft Symbol
verwendet wird, verwendet TSyringe typischerweise einen String oder eine Klasse (Klasse als "Token", die ein Interface repräsentiert) zur Registrierung. Für Interfaces ist InjectionToken
der bevorzugte Weg.
// interfaces.ts import { InjectionToken } from "tsyringe"; export interface ILogger { log(message: string): void; } export const ILoggerToken: InjectionToken<ILogger> = "ILogger";
2. Implementieren des Dienstes:
TSyringe verwendet den @injectable()
-Decorator, ähnlich wie InversifyJS.
// services.ts import { injectable, registry } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; @injectable() // Registriert diese Klasse automatisch als Implementierung für ILoggerToken // Dies ersetzt explizite Bindungsaufrufe in einem zentralen Container für einfache Fälle @registry([{ token: ILoggerToken, useClass: ConsoleLogger }]) export class ConsoleLogger implements ILogger { public log(message: string): void { console.log(`[ConsoleLogger (TSyringe)] ${message}`); } } @injectable() export class FileLogger implements ILogger { public log(message: string): void { console.log(`[FileLogger (TSyringe)] Saving to file: ${message}`); // In einer realen App würde dies in eine Datei schreiben } }
Beachten Sie den @registry
-Decorator. Dies ist ein praktischer Weg, eine konkrete Klasse direkt auf ein Interface-Token auf Klassendefinitionsebene zuzuordnen. Dies kann die Einrichtung für komplexe Anwendungen vereinfachen. Alternativ können Sie container.register
manuell verwenden, was wir unten sehen werden.
3. Definieren eines Dienstes, der den Logger verwendet:
TSyringe verwendet @inject()
, wie InversifyJS auch, für die Konstruktorinjektion.
// app.ts import { injectable, inject } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; @injectable() export class Application { private _logger: ILogger; constructor(@inject(ILoggerToken) logger: ILogger) { this._logger = logger; } public run(): void { this._logger.log("Application started!"); } }
4. Konfigurieren des IoC-Containers (falls nicht häufig @registry
verwendet wird):
TSyringe bietet einen globalen Container, container
, den Sie importieren und verwenden können.
// main.ts (oder eine dedizierte DI-Setup-Datei) import { container } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; import { ConsoleLogger, FileLogger } from "./services"; import { Application } from "./app"; // Manuelle Registrierung (Alternative zu @registry in ConsoleLogger) // container.register(ILoggerToken, { useClass: ConsoleLogger }); // Um zu FileLogger zu wechseln: container.register<ILogger>(ILoggerToken, { useClass: FileLogger }); // Für die Application, da sie direkt injizierbar ist und Abhängigkeiten entgegennimmt, // müssen wir sie möglicherweise nicht registrieren, wenn ihre Abhängigkeiten aufgelöst werden. // Wenn Sie sie jedoch später explizit auflösen möchten, könnten Sie sie registrieren: // container.register(Application, { useClass: Application }); // oder container.resolve(Application) funktioniert direkt, wenn alle Abhängigkeiten erfüllt sind. // Holen Sie sich eine Instanz der Application vom Container const app = container.resolve(Application); // TSyringe erlaubt das direkte Auflösen von Klassen, wenn alle Konstruktorabhängigkeiten aufgelöst sind. app.run(); // Ausgabe: [FileLogger (TSyringe)] Saving to file: Application started!
Scopes
TSyringe unterstützt ebenfalls verschiedene Lebenszyklus-Umgebungen:
- Transient (Standard): Bei jeder Anforderung wird eine neue Instanz erstellt.
container.register(ILoggerToken, { useClass: ConsoleLogger });
- Singleton: Bei jeder Anforderung wird dieselbe Instanz zurückgegeben.
container.registerSingleton(ILoggerToken, ConsoleLogger); // Oder zum direkten Registrieren einer Klasse als Singleton ohne Token container.registerSingleton(Application);
- Scoped: Instanzen werden innerhalb eines bestimmten Umfangs (z. B. einer Webanforderung) gemeinsam genutzt. TSyringe ermöglicht die Erstellung von Kind-Containern zu diesem Zweck.
Beispiel für Singleton:
import { container } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; import { ConsoleLogger } from "./services"; import { Application } from "./app"; container.registerSingleton<ILogger>(ILoggerToken, ConsoleLogger); const app = container.resolve(Application); app.run(); // Ausgabe: [ConsoleLogger (TSyringe)] Application started!
Wahl zwischen InversifyJS und TSyringe
Sowohl InversifyJS als auch TSyringe sind ausgezeichnete Wahlmöglichkeiten für IoC in TypeScript-Anwendungen, aber sie weisen subtile Unterschiede auf, die Ihre Entscheidung beeinflussen könnten:
- Reife und Ökosystem: InversifyJS ist schon länger verfügbar und hat eine größere Community, mehr Beispiele und potenziell mehr Integrationen mit anderen Frameworks. TSyringe, obwohl von Microsoft unterstützt, ist in seiner Verbreitung etwas neuer.
- Leichtgewichtig vs. Funktionsreich: TSyringe gilt allgemein als leichter und hat eine einfachere API, insbesondere für grundlegendes DI. InversifyJS bietet eine reichhaltigere Palette von Funktionen, einschließlich erweiterbarer Bindungssyntaxen, Middleware für Containerauflösungen und feingranularere Kontrolle über den Bindungsprozess.
Symbol
vs.string
/class
Tokens: InversifyJS empfiehlt oftSymbol
s für Tokens, um String-Literal-Konflikte zu vermeiden, obwohl auch Strings unterstützt werden. TSyringe tendiert zu Strings oder Klassenreferenzen als Tokens, was in einfacheren Fällen manchmal lesbarer sein kann.- Konfiguration: TSyringes
@registry
-Decorator ermöglicht eine sehr prägnante Inline-Registrierung von Diensten, wodurch die Notwendigkeit großer zentraler Konfigurationsdateien für einfache Fälle reduziert wird. InversifyJS zentralisiert Bindungen typischerweise innerhalb derContainer
-Instanz. Für große Anwendungen kann eine zentralisierte Konfiguration oft von Vorteil für die Übersichtlichkeit sein. - Fehlerbehandlung: Beide Frameworks bieten gute Fehlermeldungen für nicht auflösbare Abhängigkeiten, aber ihre diagnostischen Fähigkeiten können sich geringfügig unterscheiden.
Wann Sie InversifyJS wählen sollten:
- Sie benötigen einen hochgradig konfigurierbaren und erweiterbaren IoC-Container.
- Sie erstellen eine komplexe Anwendung, bei der erweiterte Funktionen wie benutzerdefinierte Auflösungshaken, spezifische Bindungssyntaxen oder detaillierte Debug-Informationen wertvoll sind.
- Sie bevorzugen eine explizite, zentralisierte Bindungskonfiguration.
Wann Sie TSyringe wählen sollten:
- Sie legen Wert auf Einfachheit und eine leichte Lösung.
- Sie schätzen die Bequemlichkeit der Inline-Registrierung über
@registry
. - Sie arbeiten in einer Umgebung, in der eine von Microsoft unterstützte Bibliothek zusätzliche Sicherheit bietet.
- Ihre DI-Anforderungen für Konstruktorinjektion und einfache Scoping sind hauptsächlich.
Letztendlich bieten beide Frameworks die gleichen Ziele bei der effektiven Implementierung von IoC und Dependency Injection. Die Wahl hängt oft von persönlichen Vorlieben, Projektanforderungen und Vertrautheit mit ihren jeweiligen APIs ab.
Fazit
Die Implementierung von Inversion of Control mit Dependency Injection ist ein Eckpfeiler des Aufbaus robuster, wartbarer und testbarer TypeScript-Anwendungen. Durch die Entkopplung von Komponenten und die Auslagerung des Abhängigkeitsmanagements erschließen wir ein neues Maß an architektonischer Flexibilität. InversifyJS und TSyringe bieten leistungsstarke, decorator-basierte Mechanismen, um dies zu erreichen, jeweils mit eigenen Stärken und Nuancen. Egal, ob Sie sich für die funktionsreiche Erweiterbarkeit von InversifyJS oder die optimierte Einfachheit von TSyringe entscheiden, die Übernahme eines IoC-Containers in Ihre TypeScript-Projekte wird zweifellos zu saubererem Code und einem angenehmeren Entwicklungserlebnis führen. Die Möglichkeit, Implementierungen einfach auszutauschen und einzelne Codeeinheiten rigoros zu testen, sind unschätzbare Vorteile, die diese Frameworks konsequent liefern.