Inyección de Dependencias
Implementa inyección de dependencias para escribir código testeable, desacoplado y mantenible en múltiples lenguajes y frameworks.
Visión General
La Inyección de Dependencias (DI) es un patrón de diseño donde los objetos reciben sus dependencias desde fuentes externas en lugar de crearlas internamente. Desacopla componentes, hace el código testeable sin mocks y permite composición flexible de servicios.
Cuándo Usar
Usa este recurso cuando:
- Escribas tests unitarios que requieren sustituir servicios reales por dobles de prueba
- Construyas aplicaciones modulares donde los componentes no deberían conocer implementaciones concretas
- Manejes grafos de objetos complejos con dependencias transitivas
- Implementes arquitecturas de plugins o patrones de estrategia
Solución
Inyección por Constructor (TypeScript)
interface EmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
class UserService {
constructor(
private emailService: EmailService,
private userRepository: UserRepository
) {}
async register(email: string, password: string) {
const user = await this.userRepository.create({ email, password });
await this.emailService.send(email, 'Bienvenido', '¡Gracias por registrarte!');
return user;
}
}
// Cableado en producción
const userService = new UserService(
new SendGridEmailService(),
new PostgresUserRepository()
);
// Cableado en tests
const userServiceTest = new UserService(
new FakeEmailService(),
new InMemoryUserRepository()
);
Inyección por Propiedad (Python)
from typing import Protocol
class Logger(Protocol):
def log(self, message: str) -> None: ...
class ConsoleLogger:
def log(self, message: str) -> None:
print(f"[LOG] {message}")
class OrderProcessor:
logger: Logger = ConsoleLogger() # Default
def process(self, order: dict) -> None:
self.logger.log(f"Procesando orden {order['id']}")
Contenedor de DI (Java con Spring)
@Service
public class PaymentService {
private final PaymentGateway gateway;
private final FraudChecker fraudChecker;
public PaymentService(PaymentGateway gateway, FraudChecker fraudChecker) {
this.gateway = gateway;
this.fraudChecker = fraudChecker;
}
}
Explicación
La DI invierte el control: en lugar de que los componentes encuentren o creen sus dependencias, el contenedor o el llamador las provee. Esto permite:
- Testeabilidad: Intercambia servicios reales por fakes sin modificar código
- Flexibilidad: Cambia implementaciones sin tocar consumidores
- Gestión de ciclo de vida: Los contenedores pueden manejar singletons, instancias scoped y disposal
- Soporte AOP: Decoradores e interceptores pueden inyectarse transparentemente
Variantes
| Enfoque | Caso de Uso | Compromiso |
|---|---|---|
| Constructor | Dependencias obligatorias | Más explícito; mejor para testing |
| Propiedad/Setter | Dependencias opcionales | Puede crear objetos parcialmente inicializados |
| Método | Dependencias por llamada | Verboso; usado para inyección de estrategia |
| Service Locator | Código legacy | Oculta dependencias; más difícil de testear |
Lo que funciona
- Prefiere inyección por constructor: Hace las dependencias explícitas e inmutables
- Evita service locators: Ocultan dependencias y dificultan los tests
- Usa interfaces/protocolos: Depende de abstracciones, no de tipos concretos. Consulta Factory Pattern para abstracciones de creación de objetos.
- Mantén roots de composición superficiales: Cablea dependencias en el punto de entrada de la aplicación
Errores Comunes
- Explosión de constructores: Más de 5 parámetros señala una abstracción faltante
- Fugas del contenedor: Pasar el contenedor de DI a los servicios anula el propósito
- Acoplamiento al framework: Usa anotaciones estándar (@Inject) cuando sea posible
- Ignorar ciclo de vida: Servicios scoped resueltos como singletons causan fugas de memoria
- Dependencias circulares: Refactoriza en eventos o un mediador si A depende de B y B de A
Preguntas frecuentes
Contenedor de DI Ligero (TypeScript)
type Factory<T> = () => T;
class DIContainer {
private factories: Map<string, Factory<any>> = new Map();
private singletons: Map<string, any> = new Map();
private scoped: Map<string, any> = new Map();
registerTransient<T>(key: string, factory: Factory<T>): void {
this.factories.set(key, factory);
}
registerSingleton<T>(key: string, factory: Factory<T>): void {
this.factories.set(key, () => {
if (!this.singletons.has(key)) {
this.singletons.set(key, factory());
}
return this.singletons.get(key);
});
}
registerScoped<T>(key: string, factory: Factory<T>): void {
this.factories.set(key, () => {
if (!this.scoped.has(key)) {
this.scoped.set(key, factory());
}
return this.scoped.get(key);
});
}
resolve<T>(key: string): T {
const factory = this.factories.get(key);
if (!factory) {
throw new Error(`No service registered for key: ${key}`);
}
return factory();
}
beginScope(): void {
this.scoped.clear();
}
endScope(): void {
this.scoped.clear();
}
}
// Registro en el composition root
const container = new DIContainer();
container.registerSingleton('db', () => new PostgresPool({ connectionString: process.env.DB_URL }));
container.registerSingleton('emailService', () => new SendGridEmailService(process.env.SENDGRID_KEY));
container.registerScoped('userRepository', () => new PostgresUserRepository(container.resolve('db')));
container.registerScoped('userService', () =>
new UserService(container.resolve('emailService'), container.resolve('userRepository'))
);
// Uso por request
container.beginScope();
const userService = container.resolve<UserService>('userService');
await userService.register('user@example.com', 'password');
container.endScope();
Lifecycle Scoped con Context Managers (Python)
from contextlib import contextmanager
from typing import TypeVar, Callable, Dict
T = TypeVar('T')
class DIContainer:
def __init__(self):
self._factories: Dict[str, Callable] = {}
self._singletons: Dict[str, object] = {}
self._scoped: Dict[str, object] = {}
def register_singleton(self, key: str, factory: Callable[[], T]) -> None:
self._factories[key] = lambda: self._singletons.setdefault(key, factory())
def register_scoped(self, key: str, factory: Callable[[], T]) -> None:
self._factories[key] = lambda: self._scoped.setdefault(key, factory())
def register_transient(self, key: str, factory: Callable[[], T]) -> None:
self._factories[key] = factory
def resolve(self, key: str) -> T:
if key not in self._factories:
raise KeyError(f'No service registered for: {key}')
return self._factories[key]()
@contextmanager
def scope(self):
"""Context manager para scoped lifetime."""
self._scoped.clear()
try:
yield self
finally:
self._scoped.clear()
# Uso — scoped por request
container = DIContainer()
container.register_singleton('db', lambda: create_engine('postgresql://localhost/app'))
container.register_scoped('session', lambda: Session(container.resolve('db')))
container.register_transient('user_service', lambda: UserService(container.resolve('session')))
with container.scope() as scoped:
service = scoped.resolve('user_service')
service.register('user@example.com', 'password')
Patrones de Testing con DI (TypeScript)
class TestContainer extends DIContainer {
constructor() {
super();
// Override servicios reales con fakes
this.registerSingleton('db', () => new InMemoryDatabase());
this.registerSingleton('emailService', () => new FakeEmailService());
this.registerScoped('userRepository', () => new InMemoryUserRepository());
this.registerScoped('userService', () =>
new UserService(this.resolve('emailService'), this.resolve('userRepository'))
);
}
}
describe('UserService', () => {
let container: TestContainer;
let userService: UserService;
let emailService: FakeEmailService;
beforeEach(() => {
container = new TestContainer();
container.beginScope();
userService = container.resolve('userService');
emailService = container.resolve('emailService');
});
afterEach(() => {
container.endScope();
});
it('sends welcome email on register', async () => {
await userService.register('user@example.com', 'password');
expect(emailService.sentEmails).toHaveLength(1);
expect(emailService.sentEmails[0].to).toBe('user@example.com');
expect(emailService.sentEmails[0].subject).toBe('Welcome');
});
it('persists user to repository', async () => {
const user = await userService.register('user@example.com', 'password');
const repo = container.resolve('userRepository') as InMemoryUserRepository;
expect(repo.users).toHaveLength(1);
expect(repo.users[0].email).toBe('user@example.com');
});
});
Recursos Relacionados
Patrón MVC
Separa la aplicación en componentes Modelo, Vista y Controlador. Patrón de diseño arquitectural para código organizado y mantenible.
PatternPatrón Repository
Abstrae la lógica de acceso a datos detrás de una interfaz limpia. Patrón de diseño arquitectural para capas de datos testeables y mantenibles.
PatternPatrón Dependency Injection
Suministra dependencias desde fuera en lugar de crearlas internamente. Un patrón arquitectural para código desacoplado y testeable.
DocPlantilla de ADR
Una plantilla reutilizable para Architecture Decision Records que captura contexto, decisión y consecuencias.
DocPlantilla de Documentación de Esquema de Base de Datos
Una plantilla para documentar esquemas de base de datos con relaciones entre entidades, definiciones de campos e historial de migraciones.
PatternPatrón Multi-Tenant Data Isolation
Aisla datos de tenants en infraestructura compartida usando row-level security, schema-per-tenant o database-per-tenant. Un patrón para aplicaciones SaaS.