DIP: Principio de Inversión de Dependencias
DIP: Principio de Inversión de Dependencias
Llegamos a la D, la última letra de SOLID y, para muchos, la más transformadora. En el capítulo de ISP aprendimos a diseñar abstracciones pequeñas y honestas. DIP responde a la pregunta que quedaba pendiente: ¿quién debe depender de esas abstracciones, y en qué dirección? La respuesta cambia por completo cómo estructurás una aplicación, y es la puerta de entrada directa a la arquitectura hexagonal.
Definición formal
El principio, otra vez de Robert C. Martin, tiene dos partes:
a) Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones.
b) Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.
Necesitamos definir dos términos:
- Módulo de alto nivel: el que contiene la lógica de negocio, las reglas que dan valor a tu aplicación. Un
ProcesadorPedidos, unServicioDeFacturación. Es lo importante, lo que no debería cambiar porque cambió la base de datos. - Módulo de bajo nivel: los detalles de implementación. La conexión a MySQL, el cliente de SendGrid, la pasarela de Stripe. Cosas reemplazables, mecanismos.
En cristiano
Normalmente escribimos el código “hacia abajo”: la lógica de negocio importa y usa directamente la base de datos concreta, el servicio de email concreto, etc. La flecha de dependencia apunta de lo importante (negocio) hacia lo reemplazable (detalle). Eso está al revés.
DIP dice: invertí esa flecha. Introducí una abstracción (una interfaz) entre ambos. Y el detalle clave, el que le da el nombre de “inversión”: esa interfaz pertenece al módulo de alto nivel, no al de bajo nivel. El negocio declara “yo necesito algo que sepa guardar pedidos” (la interfaz), y el detalle concreto (Postgres, MySQL) se adapta a cumplir esa interfaz. El poder se invierte: manda el negocio, obedece el detalle.
La inversión, visualizada
Sin DIP, la dependencia fluye así: Negocio → Detalle. El negocio conoce y depende de MySQL. Si cambiás MySQL, tocás el negocio.
Con DIP, introducimos una interfaz que vive del lado del negocio, y la dependencia queda: Negocio → Interfaz ← Detalle. Ahora ambos apuntan a la abstracción. El detalle depende de la interfaz del negocio, no al revés. Esa flecha del detalle que ahora apunta “hacia arriba” es la inversión.
Ejemplo MALO: el procesador de pedidos acoplado
Veamos un ProcesadorPedidos que hace lo que casi todos hacemos al principio: instanciar directamente sus dependencias concretas con new.
// --- Detalles de bajo nivel (implementaciones concretas) ---
class MySQLDatabase {
guardar(tabla: string, datos: object): void {
console.log(`INSERT INTO ${tabla}...`, datos);
}
}
class SendGridEmail {
enviar(destino: string, asunto: string, cuerpo: string): void {
console.log(`SendGrid → ${destino}: ${asunto}`);
}
}
class StripeGateway {
cobrar(monto: number, tarjeta: string): boolean {
console.log(`Stripe: cobrando $${monto} a ${tarjeta}`);
return true;
}
}
// --- Módulo de ALTO nivel (lógica de negocio) ---
class ProcesadorPedidos {
// ❌ El negocio instancia los detalles concretos. Acoplamiento total.
private db = new MySQLDatabase();
private email = new SendGridEmail();
private pagos = new StripeGateway();
procesar(pedidoId: string, monto: number, tarjeta: string, cliente: string): void {
const cobrado = this.pagos.cobrar(monto, tarjeta);
if (!cobrado) throw new Error("Pago rechazado");
this.db.guardar("pedidos", { pedidoId, monto, estado: "pagado" });
this.email.enviar(cliente, "Pedido confirmado", `Tu pedido ${pedidoId} fue procesado`);
}
}
Los síntomas
Este código funciona, pero está enfermo. Los síntomas:
1. Imposible de testear en aislamiento. Cada vez que ejecutes un test de procesar, se dispara un cobro real en Stripe, un INSERT real en MySQL y un email real por SendGrid. No podés testear la lógica de negocio sin toda la infraestructura levantada. No hay forma de inyectar un doble de prueba: las dependencias están soldadas con new.
// ¿Cómo testeo esto sin cobrarle de verdad a una tarjeta?
// No puedo. El `new StripeGateway()` está enterrado adentro.
const procesador = new ProcesadorPedidos();
procesador.procesar("P-001", 100, "4242...", "[email protected]"); // 💳 cobro REAL
2. Imposible cambiar de proveedor sin cirugía. ¿Querés migrar de MySQL a Postgres? ¿De SendGrid a Amazon SES? Tenés que abrir y modificar la clase de negocio. Eso viola de paso el OCP (capítulo 3): el módulo no está cerrado a la modificación.
3. La lógica importante depende de lo reemplazable. Tu regla de negocio (cobrar → guardar → notificar) queda contaminada de conocimiento sobre SQL, sobre la API de Stripe, sobre SendGrid. Lo valioso encadenado a lo trivial.
La flecha apunta mal: ProcesadorPedidos → MySQLDatabase, ProcesadorPedidos → StripeGateway, ProcesadorPedidos → SendGridEmail. El alto nivel depende del bajo nivel. Exactamente lo que DIP prohíbe.
El refactor: abstracciones + inyección por constructor
El refactor tiene dos movimientos.
Paso 1: definir las abstracciones (que pertenecen al negocio)
Declaramos interfaces que expresan lo que el negocio necesita, en su propio lenguaje, sin mencionar MySQL ni Stripe. Fijate que aplicamos ISP: son pequeñas y cohesivas.
// Estas interfaces pertenecen al módulo de ALTO nivel.
// Hablan el lenguaje del negocio, no de la infraestructura.
interface RepositorioPedidos {
guardar(pedido: { id: string; monto: number; estado: string }): void;
}
interface EnviadorEmail {
enviar(destino: string, asunto: string, cuerpo: string): void;
}
interface PasarelaPago {
cobrar(monto: number, tarjeta: string): boolean;
}
Paso 2: inyectar las dependencias por el constructor
El negocio ya no hace new. Recibe sus dependencias desde afuera, tipadas como las abstracciones. Esto es constructor injection:
class ProcesadorPedidos {
// ✅ Depende de ABSTRACCIONES, inyectadas por constructor.
constructor(
private readonly repo: RepositorioPedidos,
private readonly email: EnviadorEmail,
private readonly pagos: PasarelaPago
) {}
procesar(pedidoId: string, monto: number, tarjeta: string, cliente: string): void {
const cobrado = this.pagos.cobrar(monto, tarjeta);
if (!cobrado) throw new Error("Pago rechazado");
this.repo.guardar({ id: pedidoId, monto, estado: "pagado" });
this.email.enviar(cliente, "Pedido confirmado", `Tu pedido ${pedidoId} fue procesado`);
}
}
Paso 3: los detalles implementan las abstracciones
Ahora los módulos de bajo nivel dependen de las interfaces del negocio (implements). La flecha se invirtió:
class RepositorioPedidosPostgres implements RepositorioPedidos {
guardar(pedido: { id: string; monto: number; estado: string }): void {
console.log("INSERT INTO pedidos (Postgres)...", pedido);
}
}
class EnviadorEmailSendGrid implements EnviadorEmail {
enviar(destino: string, asunto: string, cuerpo: string): void {
console.log(`SendGrid → ${destino}: ${asunto}`);
}
}
class PasarelaPagoStripe implements PasarelaPago {
cobrar(monto: number, tarjeta: string): boolean {
console.log(`Stripe: cobrando $${monto}`);
return true;
}
}
El armado final (composition root)
En un único lugar de la aplicación (el composition root, típicamente el arranque) se decide qué implementación concreta se usa y se inyecta:
// main.ts — el ÚNICO lugar que conoce las implementaciones concretas.
const procesador = new ProcesadorPedidos(
new RepositorioPedidosPostgres(),
new EnviadorEmailSendGrid(),
new PasarelaPagoStripe()
);
procesador.procesar("P-001", 100, "4242...", "[email protected]");
La recompensa: cambiar de proveedor y testear
Ahora sí se ve el poder de DIP.
Cambiar de proveedor sin tocar el negocio. ¿Migramos a MySQL y Amazon SES? Escribimos dos clases nuevas que implementan las mismas interfaces y cambiamos solo el composition root. ProcesadorPedidos no se entera:
class RepositorioPedidosMySQL implements RepositorioPedidos {
guardar(pedido: { id: string; monto: number; estado: string }): void {
console.log("INSERT INTO pedidos (MySQL)...", pedido);
}
}
// Solo cambia el armado. La lógica de negocio quedó intacta.
const procesador = new ProcesadorPedidos(
new RepositorioPedidosMySQL(), // ← cambió esto
new EnviadorEmailSendGrid(),
new PasarelaPagoStripe()
);
Testear con dobles de prueba, sin infraestructura. En un test inyectamos mocks que no cobran, no guardan ni envían nada real, y verificamos la lógica de negocio pura:
import { describe, it, expect, vi } from "vitest";
describe("ProcesadorPedidos", () => {
it("guarda y notifica cuando el pago es aceptado", () => {
const repo: RepositorioPedidos = { guardar: vi.fn() };
const email: EnviadorEmail = { enviar: vi.fn() };
const pagos: PasarelaPago = { cobrar: vi.fn().mockReturnValue(true) };
const procesador = new ProcesadorPedidos(repo, email, pagos);
procesador.procesar("P-001", 100, "4242...", "[email protected]");
expect(pagos.cobrar).toHaveBeenCalledWith(100, "4242...");
expect(repo.guardar).toHaveBeenCalledWith({
id: "P-001",
monto: 100,
estado: "pagado",
});
expect(email.enviar).toHaveBeenCalledOnce();
});
it("no guarda ni notifica si el pago es rechazado", () => {
const repo: RepositorioPedidos = { guardar: vi.fn() };
const email: EnviadorEmail = { enviar: vi.fn() };
const pagos: PasarelaPago = { cobrar: vi.fn().mockReturnValue(false) };
const procesador = new ProcesadorPedidos(repo, email, pagos);
expect(() => procesador.procesar("P-002", 50, "0000", "[email protected]")).toThrow(
"Pago rechazado"
);
expect(repo.guardar).not.toHaveBeenCalled();
expect(email.enviar).not.toHaveBeenCalled();
});
});
Sin un solo cobro real, sin base de datos, sin red. La lógica de negocio se testea en aislamiento total. Esto es lo que se quiere decir con “el código SOLID casi se testea solo”, tema que profundiza el capítulo 10.
Diagrama: la flecha invertida
Antes — el alto nivel depende del bajo nivel:
flowchart TD
PP["ProcesadorPedidos<br/>(alto nivel · negocio)"]
DB["MySQLDatabase<br/>(bajo nivel · detalle)"]
EM["SendGridEmail<br/>(bajo nivel · detalle)"]
PG["StripeGateway<br/>(bajo nivel · detalle)"]
PP --> DB
PP --> EM
PP --> PG
Después — ambos dependen de la abstracción; la flecha del detalle se invirtió:
flowchart TD
PP["ProcesadorPedidos<br/>(alto nivel · negocio)"]
subgraph abstracciones["Abstracciones (pertenecen al negocio)"]
R["«interface»<br/>RepositorioPedidos"]
E["«interface»<br/>EnviadorEmail"]
P["«interface»<br/>PasarelaPago"]
end
PGS["RepositorioPedidosPostgres"]
SG["EnviadorEmailSendGrid"]
ST["PasarelaPagoStripe"]
PP --> R
PP --> E
PP --> P
PGS -.implements.-> R
SG -.implements.-> E
ST -.implements.-> P
Fijate cómo las flechas de los detalles (Postgres, SendGrid, Stripe) ahora apuntan hacia arriba, hacia las abstracciones del negocio. Esa es la inversión.
DIP, IoC e inyección de dependencias: no son lo mismo
Tres términos que suelen confundirse. Conviene separarlos:
- DIP (Dependency Inversion Principle) es un principio de diseño: la regla que dice “dependé de abstracciones, no de detalles, y hacé que la abstracción pertenezca al alto nivel”. Es el qué y el por qué.
- IoC (Inversion of Control) es un patrón general: en lugar de que tu código controle el flujo y cree sus colaboradores, ese control se delega a algo externo (“no me llames, yo te llamo” — el Hollywood Principle). Es un concepto más amplio que abarca callbacks, frameworks, event loops, etc.
- Inyección de dependencias (DI) es una técnica concreta para lograr IoC: pasar las dependencias desde afuera en lugar de crearlas adentro. La constructor injection que usamos arriba es la forma más común y recomendada.
Resumido: DIP es el principio, IoC es la idea de ceder el control, y DI es la técnica que aplicamos para cumplir DIP. Un contenedor de DI (como los de NestJS, InversifyJS o tsyringe) es una herramienta que automatiza el armado del composition root, pero no lo necesitás para hacer DIP: inyectar a mano por constructor ya es suficiente.
Matices y errores comunes
No toda clase necesita una interfaz. DIP no significa “poné una interfaz delante de absolutamente todo”. Vale la pena abstraer las dependencias que cruzan un límite importante: la persistencia, servicios externos, la red, el reloj del sistema, cosas que querés poder reemplazar o mockear. Una clase auxiliar interna, estable, que nunca vas a cambiar de implementación, no gana nada con una interfaz encima: solo agrega ceremonia. Abstrae los bordes, no el interior.
Inyectar de más es un smell. Si un constructor recibe ocho dependencias, el problema probablemente no es DIP sino que esa clase viola SRP: hace demasiado. La solución no es “inyectar mejor” sino partir la clase. Volveremos sobre la sobre-ingeniería y las abstracciones prematuras en el capítulo 9 de anti-patrones.
La interfaz pertenece al cliente, no al proveedor. El error sutil más común es poner la interfaz “del lado de la infraestructura”. Si RepositorioPedidos vive en la carpeta de la base de datos, no invertiste nada. La abstracción tiene que pertenecer y estar cerca del negocio que la consume; el detalle es quien viene a implementarla. Ese es el corazón de la inversión.
Relación con los otros principios y con la hexagonal
DIP es el principio que amarra a los demás y el que conecta SOLID con la arquitectura.
- Con OCP (capítulo 3): al depender de abstracciones, el
ProcesadorPedidosqueda cerrado a la modificación pero abierto a la extensión: agregás un nuevo proveedor de pago escribiendo una clase nueva, sin tocar el negocio. DIP es, en gran medida, el mecanismo que hace posible el OCP a escala de aplicación. - Con ISP (capítulo 5): las abstracciones de las que dependés deben ser pequeñas y cohesivas para ser útiles. ISP garantiza que los “puertos” tengan el tamaño justo.
- Con la arquitectura hexagonal (capítulo 8) — el adelanto fuerte: eso que llamamos “abstracciones que pertenecen al negocio” son, exactamente, los puertos de la arquitectura de puertos y adaptadores.
RepositorioPedidoses un puerto;RepositorioPedidosPostgreses un adaptador. DIP es el motor teórico que hace que el dominio quede en el centro, aislado y sin conocer los detalles de infraestructura. Cuando llegues al capítulo 8, vas a ver que la hexagonal es, en el fondo, DIP aplicado con disciplina a toda la aplicación.
Resumen
- DIP (Uncle Bob): los módulos de alto nivel no deben depender de los de bajo nivel; ambos dependen de abstracciones. Y las abstracciones no dependen de detalles: los detalles dependen de las abstracciones.
- La inversión consiste en introducir una interfaz que pertenece al alto nivel, de modo que el detalle concreto se adapte al negocio y no al revés.
- El ejemplo malo (instanciar
new MySQLDatabase(),new StripeGateway()dentro del negocio) es imposible de testear y de cambiar de proveedor, y viola de paso OCP. - El refactor: definir abstracciones (
RepositorioPedidos,EnviadorEmail,PasarelaPago) e inyectarlas por constructor. Los detalles las implementan; el composition root arma todo en un solo lugar. - La recompensa: cambiar de proveedor tocando solo el armado, y testear con dobles de prueba sin infraestructura real.
- DIP (principio) ≠ IoC (ceder el control) ≠ inyección de dependencias (la técnica). Los tres se combinan pero no son sinónimos.
- Matices: no toda clase necesita interfaz (abstrae los bordes), inyectar de más es un smell de SRP roto, y la interfaz pertenece al cliente, no al proveedor.
- DIP es la base de la arquitectura hexagonal: los puertos son abstracciones del negocio, los adaptadores son los detalles.
Vimos los cinco principios por separado. Ahora toca la parte más reveladora: verlos trabajar juntos en el refactor de un caso real, donde cada uno resuelve lo que el anterior deja a medias.
Siguiente capítulo: SOLID en conjunto: refactor de un caso real →