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

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.

flowchart diagram: Composition Root

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

VarianteDescripciónIdeal Para
Inyección por ConstructorDependencias pasadas al crearDependencias obligatorias; servicios inmutables
Inyección por SetterDependencias establecidas despuésDependencias opcionales; reconfiguración en runtime
Inyección por InterfazDependencias vía método de interfazCiclo de vida gestionado por framework
Service LocatorLa clase pide dependencias a un registroSistemas legacy; evitar en código nuevo
DI ContainerContenedor TypeScript liviano que resuelve e inyecta dependencias automáticamenteApps 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 — A necesita B, B necesita A, 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 X en 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: A inyecta B que inyecta A. 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.