StackPractices
intermediate Por Mathias Paulenko

Patrón Dependency Injection

Suministra dependencias desde fuera en lugar de crearlas internamente. Un patrón arquitectural para código desacoplado y testeable.

Temas: design

Visión General

El Patrón Dependency Injection es un patrón arquitectural donde las dependencias se suministran a una clase desde fuera en lugar de ser creadas internamente. Esto invierte el control: la clase declara lo que necesita, y un mecanismo externo lo provee. El resultado es código débilmente acoplado y altamente testeable.

Cuándo Usarlo

Usa Dependency Injection cuando:

  • Las clases dependen de otras clases y quieres evitar acoplamiento fuerte
  • Necesitas sustituir implementaciones para testing (mocks, stubs)
  • Quieres configurar comportamiento en tiempo de ejecución o despliegue
  • Estás construyendo una arquitectura de plugins o modular
  • Quieres seguir el Principio de Inversión de Dependencias (SOLID)

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 via 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 via constructor (más común, asegura que el objeto siempre esté completamente inicializado)
  • Inyección por Setter — dependencias establecidas via setters después de la construcción (flexible, pero el objeto puede estar en estado incompleto)
  • Inyección por Interfaz — dependencias proporcionadas a través de un método de interfaz (menos común, usada en frameworks)

La idea central es Inversión de Control: en lugar de que una clase cree sus propias dependencias, se suministran externamente.

Variantes

VarianteDescripciónIdeal Para
Inyección por ConstructorDependencias pasadas al crearDependencias obligatorias; servicios inmutables
Inyección por SetterDependencias establecidas despuésDependencias opcionales; reconfiguración en tiempo de ejecución
Inyección por InterfazDependencias via método de interfazCiclo de vida gestionado por framework
Service LocatorClase pide dependencias a un registroSistemas legacy; evitar en código nuevo
Contenedor DIFramework resuelve e inyecta automáticamenteAplicaciones grandes (Spring, Angular, .NET Core)

Lo que Funciona

  • Prefiere inyección por constructor para dependencias requeridas; hace explícitas las necesidades de la clase
  • Usa interfaces o abstracciones como tipos de dependencia, no clases concretas
  • Evita service locators cuando sea posible; ocultan dependencias y dificultan testing
  • Mantén la configuración DI separada de la lógica de negocio (usa módulos, archivos de config o anotaciones)
  • Respeta la Ley de Demeter — no inyectes el contenedor mismo, solo las dependencias específicas necesarias

Errores Comunes

  • Inyectar el contenedor DI en sí mismo en lugar de dependencias específicas, creando un anti-patrón service locator
  • Usar inyección por setter para dependencias requeridas, permitiendo que objetos existan en estado incompleto
  • Sobre-ingeniería con un contenedor DI para proyectos pequeños donde el cableado manual es más simple
  • Permitir dependencias circulares entre servicios inyectados, causando fallos de inicialización
  • Olvidar registrar todas las dependencias en el contenedor, llevando a errores de resolución en tiempo de ejecución

Lectura Adicional

  • Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
  • Guías relacionadas: explora las guías de dependency-injection y decoupling para profundizar.
  • Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
  • Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica patrón dependency injection cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Temas Avanzados

Escenario: DI para Servicio de Notificaciones

// DI pattern: inyectar dependencias en lugar de crearlas
// Sin DI (acoplado)
class BadNotificationService {
  private emailProvider = new SendGridProvider();  // acoplado
  private logger = new ConsoleLogger();             // acoplado
  async notify(email: string, msg: string) {
    await this.emailProvider.send(email, msg);
    this.logger.log(`Sent to ${email}`);
  }
}

// Con DI (desacoplado)
interface EmailProvider { send(to: string, body: string): Promise<void>; }
interface Logger { log(msg: string): void; }

class GoodNotificationService {
  constructor(
    private emailProvider: EmailProvider,
    private logger: Logger
  ) {}

  async notify(email: string, msg: string) {
    await this.emailProvider.send(email, msg);
    this.logger.log(`Sent to ${email}`);
  }
}

// Composicion: elegir implementaciones al construir
const service = new GoodNotificationService(
  new SendGridProvider(),   // o new SESProvider(), o new MockProvider()
  new WinstonLogger()       // o new ConsoleLogger(), o new SilentLogger()
);

// En tests: inyectar mocks
const mockEmail: EmailProvider = { send: async (to, body) => { console.log(`Mock send to ${to}`); } };
const mockLogger: Logger = { log: (msg) => { /* spy */ } };
const testService = new GoodNotificationService(mockEmail, mockLogger);

// Tipos de inyeccion
  | Tipo | Ejemplo | Ventajas | Desventajas |
  |------|---------|----------|-------------|
  | Constructor | constructor(db: DB) | Deps obligatorias | Params largos |
  | Setter | setDB(db: DB) | Opcional, flexible | Deps pueden faltar |
  | Interface | @Injectable() | Metadata, DI container | Requiere framework |
  | Property | @Inject() | Conciso | Deps ocultas |
  | Method | process(db: DB, data) | Por llamada | Repetitivo |

Lecciones:

  • DI desacopla: el servicio no crea sus dependencias
  • Constructor injection es preferido: deps obligatorias y explicitas
  • En tests, inyectar mocks: no tocar servicios reales
  • DI container automatiza la composicion (tsyringe, InversifyJS)
  • Sin DI container: composicion manual en el entry point
  • DI vs Service Locator: DI es explicito, SL es implicito y oculto

### DI vs Service Locator: cual uso?

Usa DI: las dependencias se pasan al constructor, son visibles y obligatorias. Usa Service Locator solo en legacy: el servicio pide dependencias a un registry global. DI es explicito: ves que necesita el servicio. SL es implicito: el servicio pide dependencias internamente, ocultando acoplamiento. DI es testable; SL es dificil de testear. Prefiere DI siempre.




End of document. Review and update quarterly.

## Troubleshooting

- **Pattern does not fit the problem**: re-evaluate the forces (performance, scalability, team size, coupling).   A pattern is only appropriate when its trade-offs match your constraints.
- **Too many abstractions**: if adding a pattern increases complexity without a clear benefit, simplify.   Not every module needs a factory, decorator, or strategy.
- **Tight coupling after refactoring**: check that interfaces are stable and dependencies point inward.
- **Tests break when the design changes**: favor stable contracts over internal structure.
- **Performance regression from indirection**: measure before and after.   Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.

## Errores Comunes en Producción

- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón.

Preguntas frecuentes

¿Es este patrón adecuado para proyectos pequeños?

Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.

¿Cómo se compara este patrón con alternativas?

Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.

¿Puedo aplicar este patrón parcialmente?

Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.