StackPractices
intermediate Por Mathias Paulenko

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:

  1. Testeabilidad: Intercambia servicios reales por fakes sin modificar código
  2. Flexibilidad: Cambia implementaciones sin tocar consumidores
  3. Gestión de ciclo de vida: Los contenedores pueden manejar singletons, instancias scoped y disposal
  4. Soporte AOP: Decoradores e interceptores pueden inyectarse transparentemente

Variantes

EnfoqueCaso de UsoCompromiso
ConstructorDependencias obligatoriasMás explícito; mejor para testing
Propiedad/SetterDependencias opcionalesPuede crear objetos parcialmente inicializados
MétodoDependencias por llamadaVerboso; usado para inyección de estrategia
Service LocatorCódigo legacyOculta 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

  1. Explosión de constructores: Más de 5 parámetros señala una abstracción faltante
  2. Fugas del contenedor: Pasar el contenedor de DI a los servicios anula el propósito
  3. Acoplamiento al framework: Usa anotaciones estándar (@Inject) cuando sea posible
  4. Ignorar ciclo de vida: Servicios scoped resueltos como singletons causan fugas de memoria
  5. 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');
  });
});