Introducción: El problema del acoplamiento rígido del código
Cada desarrollador de Node.js, tarde o temprano, se enfrenta a la situación en la que un cambio en un módulo arrastra una cadena de modificaciones en una docena de otros. Esto se manifiesta especialmente al escribir pruebas unitarias: para probar un servicio, hay que levantar la base de datos, APIs externas y la caché. La raíz del problema es el acoplamiento rígido (tight coupling).
La Inyección de Dependencias (DI) es un patrón de diseño que resuelve este problema. En lugar de que una clase o función cree sus propias dependencias, las recibe desde el exterior. Esto hace que el código sea flexible, testeable y que cumpla con el principio de inversión de dependencias (Dependency Inversion Principle) de SOLID.
En este artículo analizaremos la DI en la práctica: desde la inyección manual simple hasta el uso de contenedores profesionales en Node.js.
1. Inyección manual de dependencias: el primer paso
Enfoque incorrecto: acoplamiento rígido
Consideremos un ejemplo típico de un servicio para enviar correos electrónicos:
// userService.jsconst emailService = require('./emailService');
class UserService { async registerUser(email) { // Dependencia rígida de una implementación concreta await emailService.sendWelcomeEmail(email); console.log('Usuario registrado'); }}
module.exports = UserService;Problema: al probar UserService es imposible sustituir emailService por un mock. Habrá que usar herramientas complejas como proxyquire o jest.mock.
Enfoque correcto: inyección a través del constructor
Reescribamos el servicio, pasando la dependencia a través del constructor:
// userService.jsclass UserService { constructor(emailService) { this.emailService = emailService; }
async registerUser(email) { await this.emailService.sendWelcomeEmail(email); console.log('Usuario registrado'); }}
module.exports = UserService;Ahora podemos probar el servicio fácilmente:
// userService.test.jsconst UserService = require('./userService');
const mockEmailService = { sendWelcomeEmail: jest.fn().mockResolvedValue(true)};
test('debe enviar un correo de bienvenida', async () => { const service = new UserService(mockEmailService); await service.registerUser('test@test.com'); expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith('test@test.com');});2. Tipos de Inyección de Dependencias en JavaScript
En JavaScript/Node.js existen tres formas principales de inyectar dependencias:
2.1. Inyección a través del constructor
La forma más común y predecible. Las dependencias se pasan al crear el objeto:
class DatabaseService { constructor(connection) { this.connection = connection; }
async query(sql) { return this.connection.execute(sql); }}
// Usoconst db = new DatabaseService(new MySQLConnection());2.2. Inyección a través de setters
Adecuado cuando las dependencias pueden cambiarse durante la vida del objeto:
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('Aplicación iniciada');2.3. Inyección a través de parámetros de función
Ideal para el enfoque funcional y utilidades:
function sendNotification(user, emailService, smsService) { emailService.send(user.email, 'Notificación'); smsService.send(user.phone, 'Notificación');}
// Pruebaconst mockEmail = { send: jest.fn() };const mockSms = { send: jest.fn() };sendNotification({ email: 'a@b.com', phone: '123' }, mockEmail, mockSms);