Cómo la inyección de dependencias mejora la testabilidad y la modularidad
Aprende cómo la inyección de dependencias hace que el código sea más fácil de probar, refactorizar y extender. Explora patrones prácticos, ejemplos y errores comunes a evitar.

Qué significa la inyección de dependencias (sin jerga)
Dependency Injection (DI) es una idea simple: en lugar de que un fragmento de código cree las cosas que necesita, se las das desde fuera.
Esas “cosas que necesita” son sus dependencias—por ejemplo, una conexión a la base de datos, un servicio de pagos, un reloj, un logger o un emisor de correos. Si tu código construye esas dependencias por sí mismo, queda silenciosamente atado a cómo funcionan esas dependencias.
Una analogía del mundo real
Piensa en una máquina de café en una oficina. Depende de agua, granos y electricidad.
- Si la máquina está diseñada para funcionar solo con un cartucho de agua propietario que ella misma compra, quedas atado a ese proveedor.
- Si la máquina acepta cualquier entrada de agua estándar, otra persona puede elegir la fuente—grifo, filtrada, embotellada—sin cambiar la máquina.
DI es ese segundo enfoque: la “máquina de café” (tu clase/función) se centra en hacer café (su trabajo), mientras que los “insumos” (dependencias) los proporciona quien la configura.
Qué NO es DI
DI no obliga a usar un framework específico, ni es lo mismo que un contenedor DI. Puedes hacer DI manualmente pasando dependencias como parámetros (o via constructores) y listo.
DI tampoco es “mocking”. Mockear es una forma de usar DI en pruebas, pero DI en sí es solo una decisión de diseño sobre dónde se crean las dependencias.
Por qué la testabilidad y la modularidad mejoran juntas
Cuando las dependencias se proporcionan desde fuera, tu código es más fácil de ejecutar en distintos contextos: producción, pruebas unitarias, demos y futuras funcionalidades.
Esa misma flexibilidad hace que los módulos sean más limpios: las piezas se pueden reemplazar sin reconfigurar todo el sistema. Como resultado, las pruebas son más rápidas y claras (porque puedes intercambiar sustitutos simples) y la base de código resulta más fácil de cambiar (porque las partes están menos entrelazadas).
El problema central: el acoplamiento fuerte dificulta el cambio
El acoplamiento fuerte ocurre cuando una parte de tu código decide directamente qué otras partes debe usar. La forma más habitual es sencilla: llamar a new dentro de la lógica de negocio.
Cómo la instanciación directa crea acoplamiento oculto
Imagina una función de checkout que hace new StripeClient() y new SmtpEmailSender() internamente. Al principio parece cómodo—todo lo que necesitas está ahí. Pero también ata el flujo de checkout a esas implementaciones concretas, a detalles de configuración e incluso a sus reglas de creación (claves de API, timeouts, comportamiento de red).
Ese acoplamiento es “oculto” porque no es evidente en la firma del método. La función parece que solo procesa un pedido, pero en secreto depende de pasarelas de pago, proveedores de correo y quizá de una conexión a base de datos.
Por qué las dependencias difíciles de reemplazar ralentizan los cambios
Cuando las dependencias están codificadas, incluso cambios pequeños se propagan:
- Cambiar proveedor (Stripe → Adyen) significa editar la lógica de negocio en vez de intercambiar un componente.
- Añadir caching, reintentos o logging te obliga a llevar esas preocupaciones por muchos puntos de llamada.
- Actualizar una librería puede convertirse en un refactor amplio porque la creación está dispersa.
El acoplamiento fuerte aparece como pruebas lentas o inestables
Las dependencias codificadas hacen que las pruebas unitarias ejecuten trabajo real: llamadas de red, I/O en disco, relojes, IDs aleatorios o recursos compartidos. Las pruebas se vuelven lentas porque no están aisladas, y frágiles porque los resultados dependen del tiempo, servicios externos o del orden de ejecución.
Señales de dolor a vigilar
Si ves estos patrones, es probable que el acoplamiento fuerte ya te esté costando tiempo:
- Estado global usado como dependencia implícita
- Singletons difíciles de reiniciar entre pruebas
newsalpicado “por todas partes” en la lógica central- Código que no se puede probar sin base de datos, servidor web o una API key real
La Inyección de Dependencias soluciona esto haciendo las dependencias explícitas y reemplazables—sin reescribir las reglas de negocio cada vez que cambia el mundo.
Inversión de Control: separar el “qué” del “cómo”
Inversión de Control (IoC) es un cambio de responsabilidad: una clase debe centrarse en qué necesita hacer, no en cómo obtener las cosas que necesita.
Cuando una clase crea sus propias dependencias (por ejemplo, new EmailService() o abriendo una conexión a base de datos directamente), asume calladamente dos trabajos: lógica de negocio y configuración. Eso hace que la clase sea más difícil de cambiar, reutilizar y probar.
Depender de abstracciones, no de clases concretas
Con IoC, tu código depende de abstracciones—como interfaces o pequeños tipos “contrato”—en lugar de implementaciones específicas.
Por ejemplo, un CheckoutService no necesita saber si los pagos se procesan vía Stripe, PayPal o un procesador de pruebas falso. Solo necesita “algo que pueda cobrar una tarjeta”. Si CheckoutService acepta un IPaymentProcessor, puede trabajar con cualquier implementación que cumpla ese contrato.
Esto mantiene tu lógica central estable aunque las herramientas subyacentes cambien.
Mueve la creación fuera de la clase
La parte práctica de IoC es mover la creación de dependencias fuera de la clase y pasarlas (a menudo mediante el constructor). Aquí es donde encaja la inyección de dependencias (DI): DI es una forma común de lograr IoC.
En lugar de:
- la clase eligiendo y construyendo sus colaboradores
Obtienes:
- la clase recibiendo colaboradores desde fuera
El resultado es flexibilidad: intercambiar comportamientos se convierte en una decisión de configuración, no en una reescritura.
La “composition root”: donde sucede el cableado
Si las clases no crean sus dependencias, algo debe hacerlo. Ese “algo” es la composition root: el lugar donde tu aplicación se arma—típicamente el código de arranque.
La composition root es donde decides: “En producción usa RealPaymentProcessor; en pruebas usa FakePaymentProcessor.” Mantener este cableado en un solo sitio reduce sorpresas y mantiene al resto del código centrado.
Por qué importa para pruebas y refactors
IoC hace que las pruebas unitarias sean más sencillas porque puedes proveer dobles de prueba pequeños y rápidos en lugar de invocar redes o bases de datos reales.
También hace que los refactors sean más seguros: cuando las responsabilidades están separadas, cambiar una implementación raramente obliga a cambiar las clases que la usan—siempre que la abstracción se mantenga.
Estilos comunes de DI y cuándo usar cada uno
DI no es una técnica única—es un pequeño conjunto de maneras de “alimentar” a una clase con las cosas que depende (como un logger, cliente de BD o pasarela de pago). El estilo que elijas afecta la claridad, la testabilidad y la facilidad de uso indebido.
Inyección por constructor (la opción por defecto)
Con la inyección por constructor, las dependencias son requeridas para construir el objeto. Esa es la gran ventaja: no puedes olvidarlas por accidente.
Es la mejor opción cuando una dependencia:
- Es siempre necesaria para que el objeto haga su trabajo
- Se comparte entre múltiples métodos
- Es importante validar temprano (por ejemplo, null/undefined no deberían permitirse)
La inyección por constructor suele producir el código más claro y las pruebas unitarias más directas, porque tu prueba puede pasar un falso o mock justo en el momento de creación.
Inyección por parámetro/método (para trabajo puntual)
A veces una dependencia solo se necesita para una operación—por ejemplo, un formateador temporal, una estrategia especial o un valor con alcance de petición.
En esos casos, pásala como parámetro del método. Esto mantiene el objeto más pequeño y evita “promover” una necesidad puntual a un campo permanente.
Inyección por propiedad/setter (usar con cuidado)
La inyección por setter puede ser conveniente cuando realmente no puedes proporcionar una dependencia al construir el objeto (algunos frameworks o caminos de código legacy). El compromiso es que puede ocultar requisitos: la clase parece usable aunque no esté totalmente configurada.
Eso a menudo conduce a sorpresas en tiempo de ejecución (“¿por qué esto está undefined?”) y hace las pruebas más frágiles porque la configuración es fácil de omitir.
Una regla práctica
- Si la clase no puede funcionar sin ello: inyección por constructor.
- Si es solo para una llamada o comportamiento variante: inyección por método/parámetro.
- Si debes soportar cableado tardío: inyección por setter, pero añade salvaguardas (documentación clara, validación o un chequeo fail-fast).
Cómo DI mejora las pruebas unitarias (velocidad, aislamiento, claridad)
Las pruebas unitarias son más útiles cuando son rápidas, repetibles y centradas en un comportamiento. En el momento en que una prueba “unitaria” depende de una BD real, llamada de red, sistema de archivos o reloj, tiende a ralentizarse y volverse frágil. Peor aún, las fallas dejan de ser informativas: ¿falló el código o falló el entorno?
Dependency Injection (DI) arregla esto permitiendo que tu código acepte desde fuera las cosas de las que depende (acceso a datos, clientes HTTP, proveedores de tiempo). En las pruebas puedes intercambiar esas dependencias por sustitutos ligeros.
Velocidad: mantener las pruebas en memoria
Una BD real o una API añade tiempo de preparación y latencia. Con DI, puedes inyectar un repositorio en memoria o un cliente fake que devuelva respuestas preparadas al instante. Eso significa:
- más pruebas en el mismo presupuesto de tiempo
- mayor probabilidad de ejecutarlas con frecuencia
- pipelines CI más rápidos
Aislamiento: probar una cosa a la vez
Sin DI, el código a menudo hace new() de sus propias dependencias, obligando a las pruebas a recorrer toda la pila. Con DI puedes inyectar:
- mocks para verificar interacciones (ej., “envió un correo”)
- stubs para devolver valores concretos (ej., “el usuario existe”)
- fakes con comportamiento simple (ej., almacenamiento en memoria)
Sin hacks ni switches globales—solo pasar una implementación distinta.
Claridad: Arrange–Act–Assert más simple
DI hace que la configuración sea explícita. En lugar de escarbar en archivos de configuración, cadenas de conexión o variables de entorno solo para pruebas, puedes leer un test y ver inmediatamente qué es real y qué está sustituido.
Un test típico amigable con DI se lee así:
-
Arrange: crea el servicio con un repositorio fake y un reloj stub
-
Act: llama al método
-
Assert: verifica el valor de retorno y/o las interacciones con el mock
Esa sencillez reduce el ruido y hace que los fallos sean más fáciles de diagnosticar—exactamente lo que quieres de tus pruebas unitarias.
Test seams: hacer el comportamiento intercambiable a propósito
Un test seam es una “apertura” deliberada en tu código donde puedes intercambiar un comportamiento por otro. En producción enchufas lo real. En pruebas enchufas un sustituto más seguro y rápido. DI es una de las formas más sencillas de crear estos seams sin trucos.
Dónde suelen vivir los seams
Los seams son útiles alrededor de partes difíciles de controlar en una prueba:
- Tiempo (la fecha/hora actual cambia constantemente)
- Sistema de archivos (lento, permisos, limpieza)
- Email/SMS (efectos secundarios, servicios externos)
- Pasarelas de pago (dinero real, fallos de red)
Si tu lógica de negocio llama a estas cosas directamente, las pruebas se vuelven frágiles: fallan por razones ajenas a tu lógica (problemas de red, diferencias de zona horaria, archivos faltantes) y son más difíciles de ejecutar rápidamente.
Interfaces (o contratos) convierten seams en una elección sencilla
Un seam suele tomar la forma de una interfaz—o en lenguajes dinámicos, un contrato simple como “este objeto debe tener un método now()”. La idea clave es depender de lo que necesitas, no de dónde viene.
Por ejemplo, en lugar de llamar al reloj del sistema dentro de un servicio de pedidos, puedes depender de un Clock:
- Producción:
SystemClock.now() - Prueba:
FakeClock.now()devuelve un tiempo fijo
El mismo patrón funciona para lecturas de archivos (FileStore), envío de correos (Mailer) o cobros (PaymentGateway). La lógica central permanece igual; solo cambia la implementación enchufada.
Por qué los seams mejoran las pruebas
Cuando puedes intercambiar comportamiento a propósito:
- Las pruebas son menos frágiles: sin dependencia del tiempo real, la red real o el estado compartido de la máquina.
- Los casos límite son más fáciles de cubrir: puedes simular “pago rechazado”, “timeout del proveedor de email” o “fecha de fin de mes” sin montajes complicados.
- Las fallas son más claras: si una prueba falla, suele ser porque la regla de negocio está mal, no porque el entorno falló.
Los seams bien colocados reducen la necesidad de mockear todo. En su lugar, obtienes unos pocos puntos de sustitución limpios que mantienen las pruebas rápidas, enfocadas y predecibles.
Cómo DI habilita un código más modular
Modularidad es la idea de que tu software está construido a partir de partes independientes (módulos) con límites claros: cada módulo tiene una responsabilidad enfocada y una forma bien definida de interactuar con el resto.
La inyección de dependencias (DI) apoya esto haciendo esos límites explícitos. En lugar de que un módulo cree o busque todo lo que necesita, lo recibe desde fuera. Ese pequeño cambio reduce cuánto “conoce” un módulo sobre otro.
Menor acoplamiento por diseño
Cuando el código construye dependencias internamente (por ejemplo, haciendo new de un cliente de BD dentro de un servicio), el llamador y la dependencia quedan fuertemente atados. DI te anima a depender de una interfaz (o un contrato simple), no de una implementación concreta.
Eso significa que un módulo normalmente solo necesita saber:
- qué necesita (por ejemplo,
PaymentGateway.charge()) - no cómo está implementado (Stripe vs PayPal vs sandbox)
Como resultado, los módulos cambian menos en conjunto, porque los detalles internos dejan de filtrarse a través de los límites.
Reemplazar partes sin reescribir a los llamadores
Una base de código modular debería permitirte intercambiar un componente sin reescribir a quienes lo usan. DI hace esto práctico:
- Reemplazar un emisor de email real por uno en cola
- Cambiar de un repositorio basado en archivos a uno respaldado por BD
- Introducir un decorador de caching alrededor de un servicio existente
En cada caso, los llamadores siguen usando el mismo contrato. El “cableado” cambia en un solo lugar (composition root), en vez de ediciones dispersas por todo el código.
Trabajo paralelo más fácil entre equipos
Los límites de dependencia claros facilitan que equipos trabajen en paralelo. Un equipo puede desarrollar una nueva implementación detrás de una interfaz acordada mientras otro sigue desarrollando características que dependen de esa interfaz.
DI también facilita refactors incrementales: puedes extraer un módulo, inyectarlo y reemplazarlo gradualmente—sin necesitar una reescritura de golpe.
Un ejemplo simple: antes y después
Ver DI en código hace que cale más rápido que cualquier definición. Aquí tienes un ejemplo mínimo de “antes y después” usando una funcionalidad de notificaciones.
Antes: la clase crea su propia dependencia
Cuando una clase hace new internamente, decide qué implementación usar y cómo construirla.
class EmailService {
send(to, message) {
// talks to real SMTP provider
}
}
class WelcomeNotifier {
notify(user) {
const email = new EmailService();
email.send(user.email, "Welcome!");
}
}
Dolor en las pruebas: una prueba unitaria corre el riesgo de disparar comportamiento real de correo (o requiere stubs globales incómodos).
test("sends welcome email", () => {
const notifier = new WelcomeNotifier();
notifier.notify({ email: "[email protected]" });
// Hard to assert without patching EmailService globally
});
Después: inyecta la dependencia
Ahora WelcomeNotifier acepta cualquier objeto que cumpla el comportamiento necesario.
class WelcomeNotifier {
constructor(emailService) {
this.emailService = emailService;
}
notify(user) {
this.emailService.send(user.email, "Welcome!");
}
}
La prueba se vuelve pequeña, rápida y explícita.
test("sends welcome email", () => {
const fakeEmail = { send: vi.fn() };
const notifier = new WelcomeNotifier(fakeEmail);
notifier.notify({ email: "[email protected]" });
expect(fakeEmail.send).toHaveBeenCalledWith("[email protected]", "Welcome!");
});
Añadir una nueva implementación es más simple
¿Quieres SMS más tarde? No tocas WelcomeNotifier. Solo pasas una implementación distinta:
const smsService = { send: (to, msg) => {/* SMS provider */} };
const notifier = new WelcomeNotifier(smsService);
Ese es el beneficio práctico: las pruebas dejan de pelearse con los detalles de construcción y el nuevo comportamiento se añade intercambiando dependencias en lugar de reescribir código existente.
DI manual vs contenedores DI: elegir el nivel correcto
DI puede ser tan simple como “pasar lo que necesitas a quien lo usa”. Eso es DI manual. Un contenedor DI es una herramienta que automatiza ese cableado. Ambos pueden ser buenas elecciones—la clave es elegir el nivel de automatización que encaje con tu app.
Cableado manual: explícito y fácil de razonar
Con DI manual creas objetos tú mismo y pasas dependencias por constructores (o parámetros). Es sencillo:
- Ves exactamente qué se crea y dónde.
- No hay magia oculta cuando algo falla.
- Es ideal para apps pequeñas, scripts, servicios con pocos componentes y refactors iniciales.
El cableado manual también fuerza buenas prácticas de diseño. Si un objeto necesita siete dependencias, el dolor es inmediato—y suele ser una señal para dividir responsabilidades.
Contenedores DI: menos boilerplate, mejor gestión del ciclo de vida
A medida que crecen los componentes, el cableado manual puede convertirse en un “plumbing” repetitivo. Un contenedor DI puede ayudar:
- Construyendo grafos de objetos automáticamente.
- Gestionando ciclos de vida (singleton vs por-request vs transient).
- Centralizando registros (por ejemplo, intercambiar servicios reales por dobles en ciertos entornos).
Los contenedores brillan en apps con límites claros y ciclos de vida definidos—apps web, servicios de larga ejecución o sistemas donde muchas features dependen de infraestructura compartida.
No dejes que un contenedor oculte problemas de diseño
Un contenedor puede hacer que un diseño fuertemente acoplado parezca ordenado porque el cableado desaparece. Pero los problemas subyacentes permanecen:
- demasiadas dependencias por clase
- propiedad ambigua (¿quién crea/destruye recursos?)
- patrones “service locator” que invisibilizan dependencias y complican pruebas
Si añadir un contenedor hace el código menos legible, o los devs dejan de saber qué depende de qué, probablemente has ido demasiado lejos.
Un enfoque equilibrado que escala
Empieza con DI manual para mantener la claridad mientras moldeas tus módulos. Añade un contenedor cuando el cableado sea repetitivo o la gestión del ciclo de vida sea complicada.
Una regla práctica: usa DI manual dentro de tu código core/negocio y (opcionalmente) un contenedor en el límite de la app (composition root) para ensamblarlo todo. Esto mantiene el diseño claro mientras reduces boilerplate cuando el proyecto crece.
Errores comunes (y cómo evitarlos)
DI puede facilitar pruebas y cambios—pero solo si se usa con disciplina. Aquí están las formas más comunes en que DI sale mal, y hábitos para mantenerlo útil.
Sobre-inyección (el problema del “constructor con 12 parámetros”)
Si una clase necesita una larga lista de dependencias, suele estar haciendo demasiado. Eso no es un fallo de DI—es DI que revela un olor de diseño.
Una regla práctica: si no puedes describir el trabajo de la clase en una frase, o el constructor sigue creciendo, considera dividir la clase, extraer un colaborador más pequeño o agrupar operaciones relacionadas tras una única interfaz (con cuidado—no crees servicios “dios”).
Service Locator: DI que oculta las dependencias reales
El patrón Service Locator suele verse como llamar container.get(Foo) dentro del código de negocio. Parece cómodo, pero hace las dependencias invisibles: no puedes saber lo que necesita una clase leyendo su constructor.
Las pruebas se vuelven más difíciles porque debes configurar estado global (el locator) en vez de suministrar un conjunto claro y local de fakes. Prefiere pasar dependencias explícitamente (la inyección por constructor es la más directa) para que las pruebas puedan construir el grafo con intención.
Fallos en tiempo de ejecución: registros faltantes y dependencias circulares
Los contenedores DI pueden fallar en tiempo de ejecución cuando:
- falta registrar una dependencia
- se selecciona la implementación equivocada para el entorno actual
- dos servicios dependen entre sí (directa o indirectamente), creando un ciclo
Estos problemas son frustrantes porque aparecen solo cuando el cableado se ejecuta.
Mitigaciones prácticas que funcionan
Mantén los constructores pequeños y enfocados. Si la lista de dependencias de una clase crece, trátalo como prompt para refactorizar.
Añade pruebas de integración para el cableado. Incluso una prueba ligera de la “composition root” que construya tu contenedor (o el cableado manual) puede detectar registros faltantes y ciclos temprano—antes de producción.
Finalmente, mantén la creación de objetos en un solo lugar (normalmente el startup/composition root) y evita llamadas al contenedor dentro de la lógica de negocio. Esa separación preserva el principal beneficio de DI: claridad sobre qué depende de qué.
Pasos prácticos para introducir DI en una base de código existente
DI es más fácil de adoptar si lo tratas como una serie de pequeños refactors de bajo riesgo. Empieza donde las pruebas son lentas o frágiles, y donde los cambios suelen propagarse por código no relacionado.
Una checklist rápida: dónde DI da más valor primero
Busca dependencias que dificulten las pruebas o la comprensión:
- I/O: acceso a sistema de archivos, llamadas a BD, peticiones de red
- Tiempo: “now”, zonas horarias, delays, schedulers
- Aleatoriedad: UUIDs, números aleatorios, mezclas
- APIs/SDKs externos: proveedores de pagos, servicios de correo, analytics, feature flags
Si una función no puede ejecutarse sin salir del proceso, suele ser un buen candidato.
Refactor paso a paso (patrón repetible)
- Elige un seam: selecciona una dependencia externa que actualmente se haga con
newo se llame directamente. - Extrae una interfaz (o contrato) pequeña: define el comportamiento que realmente necesitas (a menudo solo 1–3 métodos). Manténla pequeña.
- Crea una implementación real: envuelve la dependencia concreta actual detrás de esa interfaz.
- Inyéctala: pásala vía constructor o argumentos de función. Prefiere la opción más simple que encaje.
- Actualiza el cableado de producción: crea la implementación real en un solo sitio (entry point, fábrica o composition root) y pásala.
- Actualiza las pruebas: reemplaza la implementación real por un fake/stub/mock predecible.
Este enfoque mantiene cada cambio revisable y te permite parar después de cualquier paso sin romper el sistema.
Mantener módulos cohesivos mientras inyectas dependencias
DI puede convertir el código en “todo depende de todo” si inyectas demasiado.
Una buena regla: inyecta capacidades, no detalles. Por ejemplo, inyecta Clock en lugar de “SystemTime + TimeZoneResolver + NtpClient”. Si una clase necesita cinco servicios no relacionados, quizá hace demasiado—considera dividir responsabilidades.
También evita pasar dependencias a través de múltiples capas “por si acaso”. Inyecta solo donde se usan; centraliza el cableado en un lugar.
Nota sobre DI cuando generas o scaffoldas aplicaciones
Si usas un generador de código o un flujo para crear features rápidamente, DI resulta aún más valiosa porque preserva la estructura a medida que el proyecto crece. Por ejemplo, cuando equipos usan Koder.ai para crear frontends React, servicios Go y backends con PostgreSQL desde una especificación guiada por chat, mantener una composition root clara e interfaces amigables para DI ayuda a que el código generado siga siendo fácil de probar, refactorizar e intercambiar integraciones (email, pagos, almacenamiento) sin reescribir la lógica central.
La regla sigue siendo: mantén la creación de objetos y el cableado específico del entorno en el límite, y deja el código de negocio centrado en el comportamiento.
Qué medir después del cambio
Deberías poder señalar mejoras concretas:
- Pruebas unitarias más rápidas (menos espera por BD/red/tiempo)
- Pruebas más aisladas (menos setups globales y estado compartido)
- Límites más claros (contratos explícitos entre módulos)
- Cambios más fáciles (intercambiar un cliente de API o estrategia de almacenamiento con ediciones mínimas)
Si quieres un siguiente paso, documenta tu “composition root” y mantenla aburrida: un archivo que haga el cableado mientras el resto del código se concentra en comportamiento.
Preguntas frecuentes
¿Qué es la inyección de dependencias en términos sencillos?
Dependency Injection (DI) significa que tu código recibe las cosas que necesita (base de datos, logger, reloj, cliente de pagos) desde fuera en lugar de crearlas internamente.
En la práctica, eso suele verse como pasar dependencias en un constructor o como parámetros de función para que sean explícitas y reemplazables.
¿En qué se diferencia DI de la Inversión de Control (IoC)?
Inversión de Control (IoC) es la idea más amplia: una clase debe centrarse en lo que hace, no en cómo obtiene a sus colaboradores.
DI es una técnica común para lograr IoC moviendo la creación de dependencias hacia fuera y pasando las dependencias como parámetros.
¿Por qué llamar a `new` dentro de la lógica de negocio provoca acoplamiento fuerte?
Si se crea una dependencia con new dentro de la lógica de negocio, esa dependencia se vuelve difícil de reemplazar.
Eso conduce a:
- dependencia forzada de un proveedor (por ejemplo, Stripe incrustado en el checkout)
- reglas de configuración y creación dispersas
- pruebas más lentas y frágiles debido a I/O real (red/BD/archivos/tiempo)
¿Cómo hace DI que las pruebas unitarias sean más rápidas y menos frágiles?
DI ayuda a que las pruebas sean rápidas y deterministas porque puedes inyectar dobles de prueba en lugar de usar sistemas externos reales.
Intercambios comunes:
- repositorios falsos/en memoria en lugar de una BD real
- reloj simulado en lugar del tiempo del sistema
- mailer mockeado en lugar de enviar correos reales
¿Necesito un contenedor DI para usar DI?
Un contenedor DI es opcional. Empieza con DI manual (pasar dependencias explícitamente) cuando:
- la app es pequeña/mediana
- el grafo de objetos es fácil de ensamblar a mano
- quieres máxima claridad
Considera un contenedor cuando el cableado sea repetitivo o necesites gestión de ciclo de vida (singleton/por petición).
¿Cuándo debo usar inyección por constructor, método o setter?
Usa inyección por constructor cuando la dependencia sea necesaria para que el objeto funcione y se use en varios métodos.
Usa inyección por método/parámetro si solo se necesita para una llamada (por ejemplo, un valor con ámbito de petición, una estrategia puntual).
Evita inyección por setter/propiedad salvo que realmente necesites un cableado tardío; añade validación para fallar rápido si falta.
¿Qué es una “composition root” y dónde debería ubicarse?
La composición raíz (composition root) es el lugar donde ensamblas la aplicación: creas implementaciones y las pasas a los servicios que las necesitan.
Mantenla cerca del arranque de la app (punto de entrada) para que el resto del código se centre en comportamiento y no en el cableado.
¿Qué es un test seam y dónde debería crear uno?
Un test seam es un punto deliberado donde se puede intercambiar comportamiento.
Buenos lugares para seams son preocupaciones difíciles de probar:
- tiempo (
Clock.now()) - I/O (almacenamiento de archivos, cliente HTTP)
- servicios externos (pagos, email)
DI crea seams permitiendo inyectar una implementación alternativa en las pruebas.
¿Cuáles son los errores comunes de DI y cómo evitarlos?
Errores comunes:
- Sobre-inyección: un constructor con muchas dependencias suele significar que la clase hace demasiado—divide responsabilidades.
- Service Locator: llamar a
container.get()dentro de la lógica de negocio oculta dependencias reales; prefiere parámetros explícitos. - Fallos en el cableado en tiempo de ejecución: registros faltantes o dependencias circulares—añade una prueba de “ensamblado” que construya el grafo de la app.
¿Cómo puedo introducir DI en una base de código existente de forma segura?
Usa una refactorización pequeña y repetible:
- Elige una dependencia problemática (BD, reloj, cliente HTTP).
- Define una interfaz/contrato pequeño con solo lo que necesitas.
- Envuelve la implementación actual detrás de esa interfaz.
- Inyéctala vía constructor/parámetros.
- Actualiza el cableado en el arranque (composition root).
- Actualiza las pruebas para usar un fake/stub/mock.
Repite para el siguiente seam; puedes parar en cualquier momento sin necesidad de una reescritura masiva.