Alexandre Freire

Blog sobre desarrollo en Swift, iOS y Xcode

Principio de Segregación de Interfaces – SOLID

Jun 1, 2020

El Principio de Segregación de Interfaces, también conocido como Principio de Separación de Interfaces, Interface Segregation Principle o ISP, es el cuarto de los principios SOLID de la programación orientada a objetos. Éste reza:

«Los objetos no deberían verse forzados a depender de interfaces que no utilizan»

Esto viene a decir que cuando creamos interfaces (protocolos) para definir comportamientos, los objetos que se conforman a ellos no deberían verse forzados a implementar métodos que no va a utilizar.

Pero ojo, porque la definición original no se refiere a los objetos que se conforman a la interfaz en sí, si no a aquellos que utilizan la interfaz. Lee el artículo hasta el final, ya que te lo explicaré con un par de ejemplos para que entiendas la diferencia a la perfección.

Ejemplo 1 – Interface Segregation Principle

¿Recuerdas el videojuego sobre aves utilizado como ejemplo en el artículo anterior sobre el Principio de Sustitución de Liskov?

Pues como a todos los videojuegos de éxito a éste también le han empezado a salir clones (véase Flappy Bird): otros videojuegos similares, pero ligeramente diferentes.

Imagina que otro studio de videojuegos diferente decide clonarlo y empieza el desarrollo de su particular simulador de animales. Éste decide que no sólo va a haber aves en el juego, sino animales en general. Además, deciden utilizar interfaces en lugar de herencia para empezar a diseñar su juego. Es así como la versión beta del clon empieza declarando un protocolo Animal con un método run():

protocol Animal {
    func run()
}

Y los primeros animales del juego son:

class Lion: Animal {

    func run() {
        print("🦁  corriendo")
    }
}
class Dog: Animal {
    
    func run() {
        print("🐶  corriendo")
    }
}
class Cat: Animal {

    func run() {
        print("😼  corriendo")
    }
}

Genial, el simulador ya funciona con los primeros animales. Es momento de añadir más funcionalidad al juego, así que los desarrolladores hacen que los animales puedan hablar. Para ello, añaden un método speak() al protocolo Animal:

protocol Animal {
    func run()
    func speak()
}

Y sus correspondiente implementación en cada una de las clases:

class Lion: Animal {

    ...

    func speak() {
        print("Roarrrr 🦁")
    }
}
class Dog: Animal {

    ...

    func speak() {
        print("Guau! 🐶")
    }
}
class Cat: Animal {

    ...

    func speak() {
        print("Miau 😼")
    }
}

Es momento de añadir más variedad de animales: los peces, por ejemplo.

Así que lo que hace este studio es añadir un nuevo método swim() al protocolo Animal:

protocol Animal {

    func run()
    func speak()
    func swim()
}

Después de implementar el método swim() en las clases Lion, Dog y Cat (todos ellos pueden nadar) es momento de crear las primeras clases de peces:

class Salmon: Animal {

    func run() { 
        fatalError("Salmons can NOT run") 
    }

    func speak() { 
        fatalError("Salmons can NOT speak") 
    }

    func swim() { 
        print("Swiming... 🐟  ")
   }
}

¡Claro! Los peces no corren ni hablan.

Entonces este studio opta por ir lanzando errores o dejando una implementación vacía para aquellos métodos que no se pueden implementar. Exactamente el mismo problema que teníamos en el artículo anterior, pero esta vez con interfaces en lugar de herencia.

Problemas de no respetar el Principio de Segregación de Interfaces

Cuando «contaminas» una interfaz o protocolo con muchos métodos, que hacen diferentes cosas, acabas teniendo los siguientes problemas:

  • Fat interfaces (interfaces gordas): interfaces o protocolos enormes, con gran cantidad de métodos y/o variables que dificultan la legibilidad y mantenibilidad de tu código. Es como estar violando el Single Responsibility Principle, pero con las interfaces.
  • Fuerzas a los objetos que se conforman al protocolo a implementar métodos que no tienen sentido para ellos, como por ejemplo, el método correr run() para los peces

Cómo cumplir el Principio de Segregación de Interfaces:

Cuando te encuentras con una interfaz que define muchos comportamientos, es mejor separarlo (segregarlo, de ahí el nombre del principio), en interfaces o protocolos más pequeños que definen un único comportamiento.

Así, con el ejemplo anterior, podríamos romper el protocolo Animal, que actualmente describe los comportamientos de correr, hablar o nadar, en protocolos específicos para cada comportamiento: Runner, Speaker y Swimmer (corredor, hablador y nadador):

protocol Runner {
    func run()
}
protocol Speaker {
    func speak()
}
protocol Swimmer {
    func swim()
}

Después, hacer que los objetos se conformen a los protocolos que necesita. Esta técnica se conoce como Composición de protocolos (hablaré sobre ello en futuros posts si te interesa):

class Lion: Runner, Speaker, Swimmer { 
    ... 
}
class Salmon: Swimmer { 
    ... 
}

Ejemplo 2 – Principio de Segregación de Interfaces

Aunque el Ejemplo 1 es un caso práctico válido de cómo aplicar el Principio de Segregación de Interfaces, la definición original del principio no se refiere a los objetos que se conforman al protocolo o interfaz, si no a aquellos que la usan.

Imagina que hay una aplicación sobre este blog en la que se persisten los artículos para poder leerlos offline. Para ello, he hecho una abstracción de la capa de persistencia mediante un protocolo llamado Database:

protocol Database {
    func all() -> [Post]
    func post(with id: String) -> Post?
    func save(post: Post)
}

Ahora podría utilizar el objeto Database en el PostListViewController para mostrar una lista de artículos:

final class PostListViewController: UIViewController {
    private let database: Database

    init(database: Database) {
        self.database = database
        super.init(nibName: nil, bundle: nil)
    }

    ...
}

Suponiendo que la aplicación sobre el blog utiliza CoreData como motor de persistencia, lo único que tendríamos que hacer es que crear una implementación del protocolo Database que, por debajo, utilice CoreData e inyectar esta implementación en el PostListViewController a través de su método init.

¿Por qué se está violando el Interface Segregation Principle en este ejemplo?

Recuerda que la definición del principio no se refiere a los objetos que se conforman al protocolo, sino a los objetos que lo utilizan:

«Los objetos no deberían verse forzados a depender de interfaces que no utilizan»

¿Utiliza el PostListViewController los comportamientos de lectura y escritura en una base de datos?

¡No!

Sólo utiliza los métodos de lectura para mostrarlos todos en una lista, y para mostrar una vista previa del detalle del artículo seleccionado.

Sin embargo, ahora mismo el PostListViewController tiene «el poder» de escritura también: tiene acceso al método save(post:). Un objeto no debería tener acceso a aquellos métodos que no va a utilizar.

¿Cómo solucionar el problema y cumplir con el Principio de Segregación de Interfaces?

Lo que vamos a hacer es separar (segregar) el protocolo Database en dos: ReadableDatabase (de lectura) y WritableDatabase (de escritura):

protocol ReadableDatabase {
    func all() -> [Post]
    func post(with id: String) -> Post?
}
protocol WritableDatabase {
    func save(post: Post)
}

Ahora inyectamos la base de datos de sólo lectura en el PostListViewController:

class PostListViewController: UIViewController {
    private let database: ReadableDatabase

    init(database: ReadableDatabase) {
        self.database = database
        super.init(nibName: nil, bundle: nil)
    }

    ...
}

Y así hacemos que éste sea completamente readonly.

En Swift, podemos «recuperar» el protocolo Database anterior componiendo varios protocolos mediante typealias:

typealias Database = ReadableDatabase & WritableDatabase

Y utilizarlo allí donde necesitemos ambos comportamientos de lectura y escritura, como por ejemplo, en el EditPostViewController.

Ejemplo donde Apple aplica el Interface Segregation Principle:

¿Alguna vez has utilizado el protocolo Decodable para convertir un JSON en un objeto Swift? ¿Y el protocolo Encodable para convertir un objeto Swift en un JSON?

Algunas veces habrás utilizado sólo uno. Otras veces el otro. Eso ocurre porque no todos los objetos necesitan los comportamientos de los dos protocolos. Por eso Swift tiene dos protocolos diferentes: Decodable y Encodable.

Cuando un objeto necesita conformarse a los dos, lo que sueles utilizar es Codable, ¿verdad? ¿Se te ocurre cómo está declarado el protocolo Codable? 👇👇

typealias Codable = Decodable & Encodable

Cómo detectar que estás violando el Principio de Segregación de Interfaces

Cuando, a la hora de conformarse a un protocolo observas que te obliga a implementar propiedades o métodos que tu objeto no necesita, y te ves obligado a lanzar errores o dejarlos vacíos, es casi seguro que estás violando el Principio de Segregación de Interfaces.

En estos casos, es mejor tener Interfaces más pequeñas que definan comportamientos únicos en lugar de una interfaz enorme y más general. Un objeto que necesite implementar todos los métodos podrá conformarse a varios protocolos sin problema.

También, cuando detectas que un objeto depende de un protocolo pero sólo utiliza una parte de todos los métodos definidos en él, es más que probable que también estés violando el Principio de Separación de Interfaces.

Recuerda, no hay nada malo en tener muchos protocolos que definan un único comportamiento.

Únete a la newsletter para no perderte ningún nuevo tutorial: recibe un email cuando se publiquen nuevos artículos o vídeos y no pierdas la oportunidad de seguir aprendiendo. 

Conclusión

El Principio de Segregación de Interfaces ayuda a poder crear nuestra arquitectura utilizando la composición de protocolos o interfaces, evitando así los Fat Interfaces que en la mayoría de los casos obligan a dejar métodos vacíos o lanzar errores en aquellos métodos que no tiene sentido implementar.

¿A dónde puedes ir ahora?

¿Conocías este principio? Cuéntame qué te ha parecido en los comentarios, y si te ha resultado útil, ¿me ayudas a compartir?

👇👇👇

¿Me ayudas a compartir en redes sociales?
Share on Facebook
Facebook
Pin on Pinterest
Pinterest
Tweet about this on Twitter
Twitter
Share on LinkedIn
Linkedin
Buffer this page
Buffer
Share on Reddit
Reddit

0 comentarios

Enviar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *