DIP: Principio de Inversión de Dependencias

Por: Artiko
soliddipinyección-dependenciasabstraccionestypescript

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:

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:

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.

Resumen

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 →