Dependency Injection in JavaScript: Ein praktischer Leitfaden für Node.js

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Einleitung: Das Problem der festen Kopplung von Code



Jeder Node.js-Entwickler stößt früher oder später auf die Situation, dass eine Änderung in einem Modul eine Kette von Anpassungen in einem Dutzend anderer nach sich zieht. Besonders deutlich wird dies beim Schreiben von Unit-Tests: Um einen Dienst zu testen, muss man eine Datenbank, externe APIs und einen Cache hochfahren. Die Wurzel des Problems ist die feste Kopplung (tight coupling).



Dependency Injection (DI) ist ein Entwurfsmuster, das dieses Problem löst. Anstatt dass eine Klasse oder Funktion ihre Abhängigkeiten selbst erstellt, erhält sie diese von außen. Dies macht den Code flexibel, testbar und entspricht dem Prinzip der Abhängigkeitsinversion (Dependency Inversion Principle) aus SOLID.



In diesem Artikel werden wir DI in der Praxis behandeln: von der einfachen manuellen Injektion bis hin zur Verwendung professioneller Container in Node.js.



1. Manuelle Abhängigkeitsinjektion: Der erste Schritt



Schlechter Ansatz: Feste Kopplung

Betrachten wir ein typisches Beispiel eines Dienstes zum Senden von E-Mails:



// userService.jsconst emailService = require('./emailService');

class UserService { async registerUser(email) { // Feste Abhängigkeit von einer bestimmten Implementierung await emailService.sendWelcomeEmail(email); console.log('Benutzer registriert'); }}

module.exports = UserService;


Problem: Beim Testen von UserService kann emailService nicht durch einen Mock ersetzt werden. Man müsste komplexe Werkzeuge wie proxyquire oder jest.mock verwenden.



Guter Ansatz: Injektion über den Konstruktor

Schreiben wir den Dienst um, indem wir die Abhängigkeit über den Konstruktor übergeben:



// userService.jsclass UserService {  constructor(emailService) {    this.emailService = emailService;  }

async registerUser(email) { await this.emailService.sendWelcomeEmail(email); console.log('Benutzer registriert'); }}

module.exports = UserService;


Jetzt können wir den Dienst leicht testen:



// userService.test.jsconst UserService = require('./userService');

const mockEmailService = { sendWelcomeEmail: jest.fn().mockResolvedValue(true)};

test('sollte eine Willkommens-E-Mail senden', async () => { const service = new UserService(mockEmailService); await service.registerUser('test@test.com'); expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith('test@test.com');});


2. Arten der Dependency Injection in JavaScript



In JavaScript/Node.js gibt es drei Hauptmethoden zur Abhängigkeitsinjektion:



2.1. Injektion über den Konstruktor

Die gebräuchlichste und vorhersagbarste Methode. Abhängigkeiten werden bei der Erstellung des Objekts übergeben:



class DatabaseService {  constructor(connection) {    this.connection = connection;  }

async query(sql) { return this.connection.execute(sql); }}

// Verwendungconst db = new DatabaseService(new MySQLConnection());


2.2. Injektion über Setter

Geeignet, wenn Abhängigkeiten während der Lebensdauer des Objekts geändert werden können:



class LoggerService {  setLogger(logger) {    this.logger = logger;  }

log(message) { this.logger.write(`[${new Date().toISOString()}] ${message}`); }}

const logger = new LoggerService();logger.setLogger(new ConsoleLogger());logger.log('Anwendung gestartet');


2.3. Injektion über Funktionsparameter

Ideal für den funktionalen Ansatz und Hilfsfunktionen:



function sendNotification(user, emailService, smsService) {  emailService.send(user.email, 'Benachrichtigung');  smsService.send(user.phone, 'Benachrichtigung');}

// Testenconst mockEmail = { send: jest.fn() };const mockSms = { send: jest.fn() };sendNotification({ email: 'a@b.com', phone: '123' }, mockEmail, mockSms);


3.

Blogs

Buchempfehlungen