Patrón Dependency Injection
Suministra dependencias desde fuera en lugar de crearlas internamente. Un patrón arquitectural para código desacoplado y testeable.
Visión General
Una clase que construye sus propios colaboradores es una clase que no puedes testear sin arrastrar el mundo real al test. El Patrón Dependency Injection es un patrón arquitectural donde las dependencias se suministran a una clase desde fuera en lugar de crearse internamente — la clase declara lo que necesita y el llamador se lo entrega.
Esa inversión de control es lo que hace el código débilmente acoplado y testeable: cambia StripeProcessor por MockProcessor y el servicio bajo prueba ni lo nota. Cuando el grafo crece, un framework o un DI container en TypeScript puede automatizar el cableado — pero el patrón funciona bien sin uno.
Cuándo Usarlo
Usa Dependency Injection cuando:
- Las clases dependen de otras clases y quieres evitar el acoplamiento fuerte.
- Necesitas sustituir implementaciones para testing (mocks, stubs, fakes).
- El comportamiento debe configurarse en despliegue o en runtime — proveedor de pagos, backend de storage, feature flags.
- Estás construyendo una arquitectura de plugins o modular donde los módulos registran sus capacidades.
- Quieres seguir el Principio de Inversión de Dependencias — consulta los principios SOLID.
Evítalo cuando una clase tiene una sola dependencia pequeña y estable, sin necesidad de simularla en tests — un new directo es más simple que tres capas de indirección. Si tu objeto necesita crear instancias en vez de recibirlas, mira el Patrón Factory.
Solución
Python
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount: float) -> bool:
pass
class StripeProcessor(PaymentProcessor):
def charge(self, amount: float) -> bool:
print(f"Cobrando ${amount} via Stripe")
return True
class PayPalProcessor(PaymentProcessor):
def charge(self, amount: float) -> bool:
print(f"Cobrando ${amount} via PayPal")
return True
class OrderService:
def __init__(self, processor: PaymentProcessor):
# Dependencia inyectada por constructor
self.processor = processor
def checkout(self, amount: float) -> bool:
return self.processor.charge(amount)
# Uso: intercambiar implementaciones fácilmente
stripe_service = OrderService(StripeProcessor())
stripe_service.checkout(100.0)
# Testing: inyectar un mock
class MockProcessor(PaymentProcessor):
def charge(self, amount: float) -> bool:
return True
test_service = OrderService(MockProcessor())
assert test_service.checkout(1.0)
JavaScript
class StripeProcessor {
charge(amount) {
console.log(`Cobrando $${amount} via Stripe`);
return true;
}
}
class PayPalProcessor {
charge(amount) {
console.log(`Cobrando $${amount} via PayPal`);
return true;
}
}
class OrderService {
constructor(processor) {
this.processor = processor;
}
checkout(amount) {
return this.processor.charge(amount);
}
}
// Uso
const stripeService = new OrderService(new StripeProcessor());
stripeService.checkout(100.0);
// Testing con mock
class MockProcessor {
charge(amount) { return true; }
}
const testService = new OrderService(new MockProcessor());
console.assert(testService.checkout(1.0));
Java
public interface PaymentProcessor {
boolean charge(double amount);
}
public class StripeProcessor implements PaymentProcessor {
public boolean charge(double amount) {
System.out.println("Cobrando $" + amount + " via Stripe");
return true;
}
}
public class PayPalProcessor implements PaymentProcessor {
public boolean charge(double amount) {
System.out.println("Cobrando $" + amount + " via PayPal");
return true;
}
}
public class OrderService {
private final PaymentProcessor processor;
// Inyección por constructor
public OrderService(PaymentProcessor processor) {
this.processor = processor;
}
public boolean checkout(double amount) {
return processor.charge(amount);
}
}
// Uso
OrderService stripeService = new OrderService(new StripeProcessor());
stripeService.checkout(100.0);
Explicación
Dependency Injection tiene tres formas comunes:
- Inyección por Constructor — dependencias pasadas por el constructor. La forma más común: el objeto no puede existir en estado incompleto y sus necesidades se ven en la firma.
- Inyección por Setter — dependencias establecidas después de construir. Flexible para dependencias opcionales, pero el objeto puede correr medio configurado.
- Inyección por Interfaz — dependencias provistas a través de un método de interfaz que la clase implementa. Menos común; sobre todo un mecanismo de frameworks.
La idea central es la Inversión de Control: en lugar de que una clase cree sus propias dependencias, se las suministra externamente — normalmente desde un único lugar llamado composition root, en el punto de entrada de la aplicación.
Los trade-offs son reales. Cada dependencia inyectada es un parámetro más del constructor — servicios que reciben cinco o seis dependencias son un smell, no una feature, y normalmente significan que la clase tiene demasiadas responsabilidades. Además, DI mueve los errores de cableado del compile time al runtime una vez que hay un contenedor: una registration que falta solo explota cuando ese camino de código corre.
Variantes
| Variante | Descripción | Ideal Para |
|---|---|---|
| Inyección por Constructor | Dependencias pasadas al crear | Dependencias obligatorias; servicios inmutables |
| Inyección por Setter | Dependencias establecidas después | Dependencias opcionales; reconfiguración en runtime |
| Inyección por Interfaz | Dependencias vía método de interfaz | Ciclo de vida gestionado por framework |
| Service Locator | La clase pide dependencias a un registro | Sistemas legacy; evitar en código nuevo |
| DI Container | Contenedor TypeScript liviano que resuelve e inyecta dependencias automáticamente | Apps grandes sin un framework pesado |
DI vs Service Locator
Resuelven el mismo problema desde direcciones opuestas. Con DI, las dependencias llegan por el constructor — visibles y obligatorias, así que ves lo que un servicio necesita desde su firma. Con Service Locator, el servicio pide las dependencias a un registro global internamente — el acoplamiento queda oculto y los tests no pueden sustituir la dependencia sin conocer el locator. Prefiere DI; guarda el locator para seams de código legacy.
Lo que Funciona
- Prefiere inyección por constructor para dependencias requeridas; la firma se convierte en el contrato.
- Depende de interfaces o abstracciones, no de clases concretas — eso es lo que permite la sustitución.
- Evita service locators en código nuevo; ocultan dependencias y dificultan el testing.
- Mantén el composition root separado de la lógica de negocio — un solo lugar que conoce las clases concretas; todo lo demás ve interfaces.
- No inyectes el contenedor mismo — eso convierte el DI de vuelta en service location. Inyecta solo las dependencias específicas que la clase necesita.
Errores Comunes
- Inyectar el contenedor DI en vez de dependencias específicas — reinventaste el anti-patrón service locator.
- Usar inyección por setter para dependencias requeridas, dejando que los objetos existan en estado incompleto.
- Meter un framework DI en un proyecto pequeño donde el cableado manual en el entry point es más simple.
- Permitir dependencias circulares entre servicios —
AnecesitaB,BnecesitaA, y nada se inicializa. - Olvidar registrar una dependencia en el contenedor — con cableado manual el compilador lo detecta; con contenedor explota en runtime.
Solución de Problemas
No registration for type Xen runtime: al contenedor le falta un binding. Regístralo en el composition root, o añade una validación eager al arrancar para que las registrations faltantes fallen rápido en vez de a mitad de un request.- Errores de dependencia circular:
AinyectaBque inyectaA. Rompe el ciclo — normalmente un lado debería escuchar eventos en vez de llamar al otro directamente, o la lógica compartida pertenece a un tercer servicio. - Constructor con seis o más parámetros: la clase tiene demasiadas responsabilidades. Divídela, o agrupa dependencias cohesionadas detrás de una interfaz facade.
- Los tests siguen pegándole a servicios reales: la dependencia no está realmente inyectada — hay un
new Concrete()escondido dentro de la clase. Busca en el código llamadas al constructor de las implementaciones de las interfaces. - Todo quedó inyectado como singleton y el estado se filtra entre requests: revisa los scopes. Las dependencias por-request (como una sesión de DB) registradas como singletons comparten estado mutable entre usuarios.
Puntos Clave
- DI invierte el control: la clase declara sus dependencias, el composition root las suministra.
- La inyección por constructor es el default — hace visibles las dependencias requeridas y los objetos siempre quedan inicializados.
- El beneficio es testabilidad y configurabilidad: cambia colaboradores reales por mocks sin tocar la clase.
- Un contenedor DI ayuda a escala pero no es obligatorio — el cableado manual le gana a un framework en apps pequeñas.
Errores Comunes en Producción
- Registrar todo como singleton y filtrar estado por-request entre usuarios — una sesión de DB o contexto de request retenido por un servicio compartido es un incidente clásico.
- La inyección por constructor creciendo a más de seis parámetros sin que nadie lo cuestione, porque el contenedor esconde el smell.
- Dependencias circulares que pasan los tests locales y se bloquean o hacen stack overflow al resolver en producción.
- Dejar que el contenedor se filtre a clases de negocio vía
container.resolve()— el anti-patrón service locator disfrazado de DI. - Registrations faltantes que solo fallan en el camino de código que nadie toca en staging — añade una pasada de validación al arranque que resuelva todo el grafo.
- Sobre-abstraer: una interfaz más un registro DI para algo que nunca tendrá una segunda implementación.
Preguntas frecuentes
¿Es DI lo mismo que Inversión de Control?
DI es una forma específica de IoC. IoC es el principio más amplio de ceder el control a código externo — el framework te llama a ti en vez de lo contrario. DI lo logra inyectando dependencias desde fuera en vez de dejar que las clases construyan las suyas.
¿Necesito un framework de DI?
No. Para proyectos pequeños alcanza con inyección manual por constructor en el entry point — consulta DI Container en TypeScript para una implementación liviana cuando lo superes. Spring, el injector de Angular o InversifyJS empiezan a rendir cuando tienes decenas de servicios interdependientes.
¿Cómo ayuda DI con el testing?
Al depender de abstracciones, puedes inyectar mocks o stubs en los tests — consulta unit testing para los patrones. La clase bajo prueba nunca toca bases de datos, pasarelas de pago ni clocks reales.
¿Qué pasa cuando un constructor recibe demasiadas dependencias?
Eso es constructor explosion — señal de que la clase maneja demasiado comportamiento. Divídela por responsabilidad, o agrupa dependencias relacionadas detrás de una interfaz de más alto nivel. DI no elimina el acoplamiento; solo lo hace visible.
¿Cómo manejo las dependencias opcionales?
La inyección por setter o por método encaja mejor que un parámetro de constructor nullable — un colaborador opcional no debería ser obligatorio para construir el objeto. En contenedores, registra la dependencia condicionalmente para que la ausencia sea explícita, no un null que alguien olvida chequear.
Recursos Relacionados
Patron Factory
Crea objetos sin especificar la clase exacta a instanciar. Un patrón de diseño creacional para la creación flexible de objetos.
PatternRepository Pattern con Generics de TypeScript
Implementa el Repository pattern con generics de TypeScript para desacoplar la lógica de acceso a datos de los servicios de dominio usando interfaces.
PatternDependency Injection Container en TypeScript
Construye un DI container liviano que resuelve dependencias de clases automaticamente, habilitando aplicaciones testeables y debilmente acopladas sin frameworks pesados
PatternPatrón Strategy
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Patrón de diseño conductual para selección flexible de comportamiento.
GuideGuía de Clean Architecture: Capas y Regla de Dependencia
Guía práctica de Clean Architecture de Uncle Bob: organiza el código en capas para que frameworks, UI y bases de datos sean detalles, no dependencias.
GuideArquitectura Hexagonal — Puertos, Adaptadores y Testabilidad
Referencia Detallada de Arquitectura Hexagonal (Puertos y Adaptadores): estructura aplicaciones para aislar la lógica de dominio de frameworks, bases de datos y servicios externos.