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?
- La semana que viene finalizaré la serie SOLID con el útimo de ellos, el Principio de Inversión de Dependencias
- También puedes ir a la página principal sobre los Principios SOLID, donde encontrarás información sobre todos los principios.
¿Conocías este principio? Cuéntame qué te ha parecido en los comentarios, y si te ha resultado útil, ¿me ayudas a compartir?
👇👇👇