Introducción: ¿Por qué tu código necesita Dependency Injection?
Cada desarrollador de JavaScript, tarde o temprano, se enfrenta al problema del código "fuertemente acoplado" (tightly coupled). Imagina una clase UserService que crea internamente una instancia de Database y Logger. ¿Qué sucede cuando quieres probar UserService sin una base de datos real? ¿O reemplazar Logger por otra implementación? En el mejor de los casos, reescribirás toda la clase; en el peor, romperás toda la aplicación.
La solución a este problema es Dependency Injection (DI), o inyección de dependencias. Es uno de los principios clave de diseño que subyace al concepto de Inversion of Control (IoC). En este artículo, analizaremos qué es DI, cómo implementarlo en JavaScript puro (Node.js) sin usar frameworks, y por qué es críticamente importante para crear aplicaciones escalables y testeables.
¿Qué es Dependency Injection en términos simples?
Imagina que estás construyendo una casa. En lugar de empotrar los enchufes en las paredes para siempre, instalas conectores estándar. Hoy conectas una lámpara vieja, mañana una lámpara inteligente. No cambias las paredes, cambias las dependencias.
En programación, Dependency Injection es un patrón en el que un objeto recibe sus dependencias (otros objetos, servicios, configuraciones) desde el exterior, en lugar de crearlas él mismo. En lugar de escribir:
// ❌ Mal: acoplamiento fuerteclass UserService { constructor() { this.db = new Database('mongodb://...'); this.logger = new ConsoleLogger(); }}Escribimos:
// ✅ Bien: inyección de dependenciasclass UserService { constructor(database, logger) { this.db = database; this.logger = logger; }}Ahora UserService no sabe cómo se crean Database y Logger. Su única tarea es usarlos. Esto es inversión de control: el control sobre la creación de dependencias pasa de la clase al código que la invoca.
Tipos de inyección de dependencias
Existen tres formas principales de inyección:
- A través del constructor (Constructor Injection) — la forma más común y preferida. Las dependencias se pasan al crear el objeto.
- A través de propiedades (Property Injection) — las dependencias se establecen después de crear el objeto. Útil para dependencias opcionales.
- A través de métodos (Method Injection) — la dependencia se pasa a un método específico cuando solo se necesita para una acción.
Escribiendo un contenedor DI simple en Node.js
En proyectos grandes, gestionar las dependencias manualmente se vuelve complicado. Aquí es donde entra en juego el contenedor DI — un objeto que sabe cómo crear y almacenar todas las dependencias de tu aplicación. Escribamos un contenedor simple pero funcional en JavaScript puro.
Nuestro contenedor admitirá dos modos: singleton (una instancia para toda la aplicación) y transient (una nueva instancia en cada solicitud).
// di-container.jsclass DIContainer { constructor() { this.registry = new Map(); this.instances = new Map(); }
// Registramos una dependencia con una función de fábrica register(name, factory, { singleton = true } = {}) { this.registry.set(name, { factory, singleton }); if (singleton) { this.instances.delete(name); // Reiniciamos el caché al re-registrar } }
// Obtenemos una instancia de la dependencia resolve(name) { const item = this.registry.get(name); if (!item) { throw new Error(`Dependencia "${name}" no encontrada`); }}