Dependency Injection in TypeScript: von der Theorie zur Praxis
Dependency Injection (DI) ist eines der wichtigsten Entwurfsmuster, das es ermöglicht, flexible, testbare und wartbare Anwendungen zu erstellen. In TypeScript wird die Implementierung von DI dank der strengen Typisierung und der Möglichkeiten von Dekorateuren besonders elegant. In diesem Artikel werden wir untersuchen, wie man Dependency Injection in der Praxis mit TypeScript implementiert, und betrachten, warum dieses Muster ein Eckpfeiler der modernen Entwicklung ist.
Was ist Dependency Injection und warum wird sie benötigt?
Dependency Injection (Abhängigkeitseinspritzung) ist ein Muster, bei dem ein Objekt seine Abhängigkeiten von außen erhält, anstatt sie selbst zu erstellen. Anstatt new SomeService() innerhalb einer Klasse zu schreiben, übergeben wir eine fertige Instanz über den Konstruktor oder eine Methode.
Die Hauptvorteile von DI:
- Reduzierte Kopplung (Coupling) — Klassen hängen nicht von konkreten Implementierungen ab, sondern nur von Abstraktionen (Schnittstellen).
- Vereinfachtes Testen — wir können reale Dienste leicht durch Mocks oder Stubs ersetzen.
- Erhöhte Wiederverwendbarkeit — Abhängigkeiten können ausgetauscht werden, ohne den Code des Verbrauchers zu ändern.
- Einhaltung des Prinzips der Abhängigkeitsinversion (DIP aus SOLID) — Module höherer Ebene hängen nicht von Modulen niedrigerer Ebene ab.
Implementierung von Dependency Injection in TypeScript ohne Frameworks
Beginnen wir mit einem einfachen Beispiel. Angenommen, wir haben einen Logging-Dienst und eine Klasse, die ihn verwendet.
// Ohne DI - feste Kopplungclass Logger { log(message: string): void { console.log(`[LOG]: ${message}`); }}
class UserService { private logger: Logger;
constructor() { this.logger = new Logger(); // Problem: feste Abhängigkeit }
createUser(name: string): void { this.logger.log(`Benutzer erstellt: ${name}`); // ... Erstellungslogik }}
// Mit DI - über den Konstruktorinterface ILogger { log(message: string): void;}
class ConsoleLogger implements ILogger { log(message: string): void { console.log(`[CONSOLE LOG]: ${message}`); }}
class FileLogger implements ILogger { log(message: string): void { // Logik zum Schreiben in eine Datei console.log(`[FILE LOG]: ${message}`); }}
class UserServiceWithDI { constructor(private logger: ILogger) {} // Abhängigkeit wird von außen eingespritzt
createUser(name: string): void { this.logger.log(`Benutzer erstellt: ${name}`); }}
// Verwendungconst consoleLogger = new ConsoleLogger();const userService = new UserServiceWithDI(consoleLogger);userService.createUser('Alice'); // [CONSOLE LOG]: Benutzer erstellt: AliceWie Sie sehen, weiß UserServiceWithDI jetzt nicht, welcher Logger verwendet wird. Wir können ihn leicht durch FileLogger oder einen Mock für Tests ersetzen.
Fortgeschrittenes DI: Erstellen eines einfachen IoC-Containers
Wenn es viele Abhängigkeiten gibt, wird die manuelle Einspritzung über den Konstruktor umständlich. Hier kommt der IoC-Container (Inversion of Control Container) zur Hilfe. Er verwaltet die Erstellung und den Lebenszyklus von Objekten.
Wir implementieren einen einfachen Container in TypeScript:
// Typ für die Abhängigkeitsfabriktype Factory<T> = (container: Container) => T;
class Container { private factories = new Map<string, Factory<any>>(); private instances = new Map<string, any>();
// Registrierung einer Abhängigkeit (Fabrik wird bei jeder Anfrage aufgerufen) register<T>(name: string, factory: Factory<T>): void { this.factories.set(name, factory); }
// Registrierung eines Singletons (Fabrik wird einmal aufgerufen) singleton<T>(name: string, factory: Factory<T>): void { this.factories.set(name, factory); }
// Abrufen einer Instanz get<T>(name: string): T { if (this.instances.has(name)) { return this.instances.get(name); } const factory = this.factories.get(name); if (!factory) { throw new Error(`Abhängigkeit ${name} nicht registriert`); } const instance = factory(this); this.instances.set(name, instance); return instance; }}
// Verwendungconst container = new Container();container.register('ILogger', () => new ConsoleLogger());container.register('UserService', (c) => new UserServiceWithDI(c.get('ILogger')));
const service = container.get('UserService');service.createUser('Bob');Jetzt müssen wir uns nicht mehr um die manuelle Erstellung von Objekten kümmern. Der Container erledigt das für uns.