Principio de Inversión de Dependencia (DIP)

El principio de inversión de dependencia establece que los módulos de alto nivel, cuando dependen de módulos de bajo nivel, deben hacerlo mediante abstracciones, como interfaces, en vez de depender directamente, de tal manera que una clase de alto nivel no esté acoplada con una implementación concreta de una faceta de la aplicación, sino con una interfaz que pueda ser aplicada por otras facetas compatibles.

El principio de inversión de dependencia, conocido como Dependency Inversion Principle dentro de los cinco principios SOLID, es fundamental para construir un código más organizado, flexible y fácil de mantener. Este principio nos indica que los módulos de alto nivel no deberían depender directamente de módulos de bajo nivel, sino que ambos deberían depender de abstracciones, como interfaces. Esto evita que nuestro código quede rígido y difícil de modificar cuando cambian los detalles concretos.

Para entenderlo mejor, imaginemos que tenemos un módulo de pago que es el encargado de gestionar diferentes métodos para realizar pagos, como Paypal, Stripe, Visa o Mastercard. Si nuestro módulo de pago depende directamente de una clase concreta, por ejemplo la clase Paypal, estamos creando una dependencia fuerte hacia un módulo de bajo nivel. Esto genera problemas cuando queremos cambiar o ampliar los métodos de pago, porque cada uno puede tener su propia forma de funcionar, con métodos y parámetros diferentes. Por ejemplo, Paypal puede tener métodos para enviar o solicitar dinero, Stripe puede soportar pagos recurrentes, y Visa puede tener otro nombre para el método de pago. Si nuestro módulo de pago está acoplado a una implementación concreta, cambiar o añadir métodos de pago se vuelve complicado y propenso a errores.

La solución que propone el principio de inversión de dependencia es crear una interfaz común, por ejemplo llamada MetodoPago, que defina un método estándar como pagar. Cada clase concreta de método de pago implementará esta interfaz, adaptando su funcionamiento interno para cumplir con el contrato definido. Así, nuestro módulo de pago solo necesita conocer la interfaz y no las implementaciones concretas. Esto nos permite cambiar el método de pago simplemente pasando una instancia diferente que implemente la interfaz, sin modificar el módulo de pago.

Además, es recomendable que esta dependencia se pase al módulo de pago a través del constructor, es decir, que el módulo de pago reciba en su constructor un objeto que implemente la interfaz MetodoPago. De esta forma, evitamos que el módulo de pago cree internamente las instancias concretas, lo que haría que estuviera acoplado a ellas. Pasar la dependencia desde fuera mejora la flexibilidad y facilita la realización de pruebas, ya que podemos inyectar implementaciones falsas o mocks para testear sin necesidad de interactuar con servicios reales.

Veamos un ejemplo en código para ilustrar esta idea:

// Definimos la interfaz que abstrae el método de pago
public interface MetodoPago {
    void pagar(double cantidad);
}

// Implementación concreta para Paypal
public class Paypal implements MetodoPago {
    @Override
    public void pagar(double cantidad) {
        System.out.println("Pagando " + cantidad + " con Paypal");
        // Lógica específica de Paypal
    }
}

// Implementación concreta para Visa
public class Visa implements MetodoPago {
    @Override
    public void pagar(double cantidad) {
        System.out.println("Pagando " + cantidad + " con Visa");
        // Lógica específica de Visa
    }
}

// Módulo de pago que depende de la abstracción MetodoPago
public class ModuloPago {
    private MetodoPago metodoPago;

    // La dependencia se inyecta a través del constructor
    public ModuloPago(MetodoPago metodoPago) {
        this.metodoPago = metodoPago;
    }

    public void realizarPago(double cantidad) {
        metodoPago.pagar(cantidad);
    }
}

Con esta estructura, si queremos cambiar el método de pago, simplemente creamos una instancia diferente que implemente MetodoPago y la pasamos al constructor de ModuloPago. Por ejemplo:

MetodoPago paypal = new Paypal();
ModuloPago modulo = new ModuloPago(paypal);
modulo.realizarPago(100.0);

O para usar Visa:

MetodoPago visa = new Visa();
ModuloPago modulo = new ModuloPago(visa);
modulo.realizarPago(200.0);

Así, el módulo de pago no necesita saber nada sobre las implementaciones concretas, solo interactúa con la interfaz común. Esto hace que nuestro código sea más limpio, reutilizable y fácil de extender.

Además, esta forma de pasar las dependencias desde fuera es una forma de inyección de dependencias, que aunque es un concepto distinto, suele complementarse con el principio de inversión de dependencia para lograr un diseño aún más flexible y testable.

En definitiva, aplicar el principio de inversión de dependencia nos ayuda a evitar que los módulos de alto nivel estén atados a detalles concretos, favoreciendo que dependan de abstracciones y que las dependencias se inyecten desde fuera, lo que mejora la mantenibilidad y la capacidad de evolución de nuestro código.

Lista de reproducción
  1. 1
    Principio de Responsabilidad Única (SRP)
    8 minutos
  2. 2
    Principio Abierto-Cerrado (OCP)
    11 minutos
  3. 3
    Principio de Sustitución de Liskov (LSP)
    8 minutos
  4. 4
    Principio de Segregación de Interfaz (ISP)
    6 minutos
  5. 5
    Principio de Inversión de Dependencia (DIP)
    10 minutos