Einleitung: Was ist Dependency Injection und warum wird sie benötigt?
In der Welt der Softwareentwicklung stoßen wir oft auf Situationen, in denen eine Klasse (oder ein Modul) von einer anderen abhängt. Beispielsweise kann die Klasse UserService die Klasse DatabaseService zum Speichern von Daten verwenden. Der traditionelle Ansatz besteht darin, eine Instanz von DatabaseService direkt innerhalb von UserService zu erstellen. Dies führt zu einer starken Kopplung des Codes, was das Testen, die Wartung und die Erweiterung erschwert.
Dependency Injection (DI) ist ein Entwurfsmuster, das dieses Problem löst. Anstatt Abhängigkeiten innerhalb der Klasse zu erstellen, werden sie von außen „injiziert“. Dies implementiert das Prinzip der Inversion of Control (IoC): Nicht die Klasse verwaltet ihre Abhängigkeiten, sondern externer Code (ein Container oder die aufrufende Stelle) stellt sie bereit.
In diesem Artikel werden wir anhand einfacher Beispiele erläutern, wie Dependency Injection in TypeScript implementiert wird. Sie erfahren, warum DI als gute Praxis gilt und wie es hilft, sauberen, testbaren Code zu schreiben.
Das Problem: Starre Kopplung ohne DI
Beginnen wir mit einem Beispiel, das zeigt, wie man Code nicht schreiben sollte. Stellen Sie sich vor, wir haben eine Anwendung, die Benachrichtigungen an Benutzer sendet.
// Ohne Dependency Injectionclass EmailService { sendEmail(to: string, message: string): void { console.log(`Sende E-Mail an ${to}: ${message}`); // ... tatsächliche Sende-Logik }}
class NotificationService { private emailService: EmailService;
constructor() { // Problem: Starre Kopplung this.emailService = new EmailService(); }
notifyUser(userEmail: string, message: string): void { this.emailService.sendEmail(userEmail, message); }}
// Verwendungconst notifier = new NotificationService();notifier.notifyUser('user@example.com', 'Hallo!');In diesem Code ist NotificationService fest an EmailService gebunden. Was ist, wenn wir SMS- oder Push-Benachrichtigungen senden möchten? Der Code von NotificationService müsste geändert werden. Außerdem ist das Testen von NotificationService schwierig – wir können den echten EmailService nicht durch ein Mock-Objekt ersetzen.
Die Lösung: Abhängigkeitsinjektion über den Konstruktor
Der einfachste und gebräuchlichste Weg, DI zu implementieren, besteht darin, Abhängigkeiten über den Konstruktor zu übergeben. Dies macht den Code flexibel und leicht testbar.
Zunächst definieren wir ein Interface für unseren Benachrichtigungsdienst. Dies ist ein entscheidender Schritt: Wir programmieren auf der Ebene von Abstraktionen, nicht von konkreten Implementierungen.
// Definiert ein Interface für jeden Benachrichtigungsdienstinterface INotificationService { send(to: string, message: string): void;}
// Implementierung für E-Mailclass EmailService implements INotificationService { send(to: string, message: string): void { console.log(`[EMAIL] Gesendet an ${to}: ${message}`); }}
// Implementierung für SMSclass SMSService implements INotificationService { send(to: string, message: string): void { console.log(`[SMS] Gesendet an ${to}: ${message}`); }}
// Klasse, die DI verwendetclass UserNotifier { private notifier: INotificationService;
// Abhängigkeit wird über den Konstruktor injiziert constructor(notifier: INotificationService) { this.notifier = notifier; }
notifyUser(userContact: string, message: string): void { this.notifier.send(userContact, message); }}
// Verwendungconst emailNotifier = new UserNotifier(new EmailService());emailNotifier.notifyUser('alice@example.com', 'Ihre Bestellung ist fertig!');
const smsNotifier = new UserNotifier(new SMSService());smsNotifier.notifyUser('+7-999-123-45-67', 'Bestätigungscode: 1234');Jetzt weiß UserNotifier nicht, welcher Dienst genau