Introduction : Le problème du couplage fort du code
\n\nChaque développeur Node.js rencontre tôt ou tard une situation où une modification dans un module entraîne une cascade de corrections dans une dizaine d'autres. Cela se manifeste particulièrement lors de l'écriture de tests unitaires : pour tester un seul service, il faut démarrer une base de données, des API externes et un cache. La racine du problème est le couplage fort (tight coupling).
\n\nL'Injection de Dépendances (DI) est un patron de conception qui résout ce problème. Au lieu que la classe ou la fonction crée ses propres dépendances, elle les reçoit de l'extérieur. Cela rend le code flexible, testable et conforme au principe d'inversion des dépendances (Dependency Inversion Principle) de SOLID.
\n\nDans cet article, nous allons explorer la DI en pratique : de l'injection manuelle simple à l'utilisation de conteneurs professionnels dans Node.js.
\n\n1. Injection manuelle des dépendances : premier pas
\n\nMauvaise approche : couplage fort
\nConsidérons un exemple typique de service d'envoi d'email :
\n\n// userService.js\nconst emailService = require('./emailService');\n\nclass UserService {\n async registerUser(email) {\n // Dépendance forte à une implémentation spécifique\n await emailService.sendWelcomeEmail(email);\n console.log('Utilisateur enregistré');\n }\n}\n\nmodule.exports = UserService;\n\nProblème : lors du test de UserService, il est impossible de remplacer emailService par un mock. Il faudrait utiliser des outils complexes comme proxyquire ou jest.mock.
Bonne approche : injection via le constructeur
\nRéécrivons le service en passant la dépendance via le constructeur :
\n\n// userService.js\nclass UserService {\n constructor(emailService) {\n this.emailService = emailService;\n }\n\n async registerUser(email) {\n await this.emailService.sendWelcomeEmail(email);\n console.log('Utilisateur enregistré');\n }\n}\n\nmodule.exports = UserService;\n\nMaintenant, nous pouvons facilement tester le service :
\n\n// userService.test.js\nconst UserService = require('./userService');\n\nconst mockEmailService = {\n sendWelcomeEmail: jest.fn().mockResolvedValue(true)\n};\n\ntest('doit envoyer un email de bienvenue', async () => {\n const service = new UserService(mockEmailService);\n await service.registerUser('test@test.com');\n expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith('test@test.com');\n});\n\n2. Types d'Injection de Dépendances en JavaScript
\n\nEn JavaScript/Node.js, il existe trois principales façons d'injecter des dépendances :
\n\n2.1. Injection via le constructeur
\nLa méthode la plus courante et prévisible. Les dépendances sont passées lors de la création de l'objet :
\n\nclass DatabaseService {\n constructor(connection) {\n this.connection = connection;\n }\n\n async query(sql) {\n return this.connection.execute(sql);\n }\n}\n\n// Utilisation\nconst db = new DatabaseService(new MySQLConnection());\n\n2.2. Injection via les setters
\nConvient lorsque les dépendances peuvent être modifiées pendant le fonctionnement de l'objet :
\n\nclass LoggerService {\n setLogger(logger) {\n this.logger = logger;\n }\n\n log(message) {\n this.logger.write(`[${new Date().toISOString()}] ${message}`);\n }\n}\n\nconst logger = new LoggerService();\nlogger.setLogger(new ConsoleLogger());\nlogger.log('Application démarrée');\n\n2.3. Injection via les paramètres de fonction
\nIdéal pour l'approche fonctionnelle et les utilitaires :
\n\nfunction sendNotification(user, emailService, smsService) {\n emailService.send(user.email, 'Notification');\n smsService.send(user.phone, 'Notification');\n}\n\n// Test\nconst mockEmail = { send: jest.fn() };\nconst mockSms = { send: jest.fn() };\nsendNotification({ email: 'a@b.com', phone: '123' }, mockEmail, mockSms);\n\n