Diseño de software

[SOLID #2] SOLID en la práctica (II): ISP·DIP y por qué existe la inyección de dependencias

En la entrega anterior vimos las tres primeras letras de SOLID: SRP·OCP·LSP. Hoy resumiremos las dos restantes.

3 min de lectura
Imagen de portada de [SOLID #2] SOLID en la práctica (II): ISP·DIP y por qué existe la inyección de dependencias

En la entrega anterior vimos las tres primeras letras de SOLID: SRP·OCP·LSP. Hoy resumiremos las dos restantes.

Son ISP (principio de segregación de interfaces) y DIP (principio de inversión de dependencias).

En especial, DIP es la raíz teórica de la «inyección de dependencias (DI)» que encontramos al usar bibliotecas de DI en Swift, ya sea Swinject o Factory. Al terminar esta entrega entenderás por qué esas bibliotecas tienen esa estructura.


ISP: principio de segregación de interfaces

Veamos primero la definición.

No se debe obligar a un cliente a depender de métodos que no utiliza.

Suena abstracto, pero se entiende enseguida al ver un caso problemático. Supongamos que hemos creado un protocolo para una impresora multifunción.

protocol Machine {
    func print(_ doc: Document)
    func scan(_ doc: Document)
    func fax(_ doc: Document)
}

// Una impresora antigua solo puede imprimir...
class OldPrinter: Machine {
    func print(_ doc: Document) { /* Funcionamiento normal */ }
    func scan(_ doc: Document) { fatalError("No puede escanear") }  // Implementación forzada
    func fax(_ doc: Document) { fatalError("No puede enviar faxes") }   // Implementación forzada
}

Estamos obligando a una impresora que solo imprime a implementar también el escaneo y el fax. Al final aparece un método falso que lanza una excepción, lo que también constituye una violación de LSP, que vimos en la entrega anterior. Una interfaz demasiado grande termina infringiendo dos principios en cadena.

La solución es sencilla: dividir la interfaz por responsabilidades.

protocol Printer { func print(_ doc: Document) }
protocol Scanner { func scan(_ doc: Document) }
protocol Fax { func fax(_ doc: Document) }

class OldPrinter: Printer { ... }                      // Solo impresión
class MultiFunction: Printer, Scanner, Fax { ... }     // Impresora multifunción completa

Cada tipo promete únicamente lo que puede hacer. En la práctica, suele manifestarse así: si al adoptar un protocolo aparece una implementación vacía con el comentario «este método no tiene nada que ver con nosotros…», ha llegado el momento de dividirlo.


DIP: principio de inversión de dependencias

Es un principio que genera mucha confusión por su nombre. La definición consta de dos frases.

Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones.

Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.

Veámoslo en código. Supongamos que un servicio de pedidos utiliza directamente una biblioteca de envío de correos electrónicos.

// El módulo de alto nivel (lógica de negocio) depende directamente del módulo de bajo nivel (detalles de implementación)
import SendGrid

class OrderService {
    private let mailer = SendGridClient(apiKey: apiKey)

    func completeOrder(_ order: Order) {
        // Procesamiento del pedido...
        mailer.send(to: order.email, message: "Pedido completado")  // SendGridAcoplado
    }
}

Esta estructura tiene dos problemas. Para cambiar SendGrid por otro servicio hay que abrir la lógica de negocio y, cada vez que hacemos una prueba, se envía un correo real.

Al aplicar DIP, la dirección de la flecha cambia.

// La abstracción la define el módulo de alto nivel (dominio de pedidos)
protocol NotificationSender {
    func send(to: String, message: String) async throws
}

class OrderService {
    private let sender: NotificationSender

    init(sender: NotificationSender) {  // Depende únicamente de la abstracción
        self.sender = sender
    }

    func completeOrder(_ order: Order) async throws {
        try await sender.send(to: order.email, message: "Pedido completado")
    }
}

// Los detalles(SendGrid)siguen la abstracción
class SendGridSender: NotificationSender { ... }
class SlackSender: NotificationSender { ... }
class FakeSender: NotificationSender { ... }  // Para pruebas

Antes, la dependencia apuntaba de «servicio de pedidos → SendGrid»; ahora es «servicio de pedidos → protocolo ← SendGrid». Como los detalles se inclinan hacia el dominio, se denomina «inversión».

La inversión de la dirección de las flechas es todo lo que implica DIP
La inversión de la dirección de las flechas es todo lo que implica DIP
El cambio de dirección de las flechas es la esencia de la «inversión»
El cambio de dirección de las flechas es la esencia de la «inversión»

Por eso existen los frameworks de DI

Aquí surge una pregunta natural: «Entonces, ¿quién crea SendGridSender()

El contenedor de inyección de dependencias (DI) es quien se encarga de este ensamblaje. Bibliotecas de DI del ecosistema Swift, como Swinject o Factory, hacen exactamente eso. Las clases solo dependen de los protocolos, y la biblioteca se encarga de insertar las implementaciones reales.

DIP es el principio (la dirección), mientras que DI es la técnica (la herramienta) para llevarlo a la práctica. Entender esta distinción por sí sola puede elevar mucho la calidad de una respuesta en una entrevista.

El framework se encarga de conectar y desconectar las implementaciones, y de ensamblarlas
El framework se encarga de conectar y desconectar las implementaciones, y de ensamblarlas

Resumen completo de SOLID

Si comprimimos SOLID, explicado en dos entregas, en cinco líneas, queda así.

Principio Resumen en una línea
SRP Si las personas que piden cambios son diferentes, separa también el código
OCP En los lugares que cambian con frecuencia, responde añadiendo en vez de modificando
LSP La clase hija no debe romper el contrato de la clase padre
ISP No obligues a implementar métodos que no se utilizan
DIP No hagas que la lógica de negocio dependa de los detalles

En última instancia, los cinco apuntan en la misma dirección: reduce el alcance de propagación de los cambios.

Sin embargo, aplicar estos principios mecánicamente a todo el código puede terminar infringiendo KISS y YAGNI. Son herramientas que deben aplicarse selectivamente donde los cambios ocurren con frecuencia. Creo que ese sentido del equilibrio es la verdadera habilidad.

Seguir leyendo