Einführung in Dependency Injection in TypeScript
Dependency Injection (DI) ist eines der wichtigsten Entwurfsmuster, das es ermöglicht, lose gekoppelte, testbare und leicht wartbare Anwendungen zu erstellen. Im Kontext von TypeScript wird die Implementierung von DI dank der strengen Typisierung und der Möglichkeiten von Dekorateuren besonders elegant und sicher.
In diesem Artikel werden wir uns ansehen, was Dependency Injection ist, wie man es in TypeScript implementiert und welche Werkzeuge diesen Prozess vereinfachen können. Sie erfahren, wie DI hilft, feste Kopplungen zwischen Klassen zu vermeiden und Ihren Code flexibler zu machen.
Was ist Dependency Injection und warum wird es benötigt?
Dependency Injection (Abhängigkeitsinjektion) ist ein Muster, bei dem ein Objekt seine Abhängigkeiten von außen erhält, anstatt sie selbst zu erstellen. Dies realisiert das Prinzip Inversion of Control (IoC) – die Umkehrung der Steuerung.
Problem ohne DI:
class Logger { log(message: string): void { console.log(message); }}
class UserService { private logger: Logger;
constructor() { this.logger = new Logger(); // Feste Kopplung }
createUser(name: string): void { this.logger.log(`User ${name} created`); }}In diesem Beispiel ist UserService fest an eine bestimmte Implementierung von Logger gebunden. Wenn wir den Logger durch einen Datei- oder Remote-Logger ersetzen möchten, müssten wir den Code von UserService ändern.
Lösung mit DI:
interface ILogger { log(message: string): void;}
class ConsoleLogger implements ILogger { log(message: string): void { console.log(message); }}
class FileLogger implements ILogger { log(message: string): void { // Schreiben in Datei }}
class UserService { constructor(private logger: ILogger) {} // Abhängigkeit wird injiziert
createUser(name: string): void { this.logger.log(`User ${name} created`); }}
// Verwendungconst service = new UserService(new ConsoleLogger());Vorteile von DI:
- Testbarkeit – reale Abhängigkeiten können leicht durch Mock-Objekte ersetzt werden.
- Flexibilität – das Verhalten des Systems kann geändert werden, ohne den Code der Klassen zu ändern.
- Trennung der Verantwortlichkeiten – Klassen kümmern sich nicht um die Erstellung ihrer Abhängigkeiten.
- Wiederverwendbarkeit – Abhängigkeiten können in verschiedenen Teilen der Anwendung verwendet werden.
Manuelle Implementierung von Dependency Injection in TypeScript
Der einfachste Weg ist, Abhängigkeiten über den Konstruktor oder Eigenschaften zu injizieren. TypeScript macht dies mit seinen Interfaces und Zugriffsmodifikatoren sehr bequem.
Injektion über den Konstruktor (Constructor Injection)
interface IRepository<T> { getAll(): Promise<T[]>; getById(id: number): Promise<T | null>;}
class UserRepository implements IRepository<User> { async getAll(): Promise<User[]> { // Reale Logik für die Datenbankarbeit return []; }
async getById(id: number): Promise<User | null> { return null; }}
class UserController { constructor( private repository: IRepository<User>, private logger: ILogger ) {}
async getUsers(): Promise<User[]> { this.logger.log('Fetching users...'); return this.repository.getAll(); }}
// Erstellung des Abhängigkeitsgraphenconst logger = new ConsoleLogger();const repository = new UserRepository();const controller = new UserController(repository, logger);Injektion über Eigenschaften (Property Injection)
class EmailService { private sender!: ISender;
setSender(sender: ISender): void {