Principio de Segregación de Interfaces (SOLID 4ª parte)
Antonio Leiva

Ya estamos acabando con el repaso de los principios SOLID. Tras ver el Principio de sustitución de Liskov, hoy entramos de lleno en el Principio de segregación de interfaces.

Si quieres tenerlo más cómodo, puedes descargarte el contenido en formato PDF y leerlo donde quieras. Te he preparado esta guía de Principios SOLID para ti.

Principio de segregación de interfaces

El principio de segregación de interfaces viene a decir que ninguna clase debería depender de métodos que no usa. Por tanto, cuando creemos interfaces que definan comportamientos, es importante estar seguros de que todas las clases que implementen esas interfaces vayan a necesitar y ser capaces de agregar comportamientos a todos los métodos. En caso contrario, es mejor tener varias interfaces más pequeñas.

Las interfaces nos ayudan a desacoplar módulos entre sí. Esto es así porque si tenemos una interfaz que explica el comportamiento que el módulo espera para comunicarse con otros módulos, nosotros siempre podremos crear una clase que lo implemente de modo que cumpla las condiciones.

El módulo que describe la interfaz no tiene que saber nada sobre nuestro código y, sin embargo, nosotros podemos trabajar con él sin problemas.

El problema

La problemática surge cuando esas interfaces intentan definir más cosas de las debidas, lo que se denominan fat interfaces.

Probablemente ocurrirá que las clases hijas acabarán por no usar muchos de esos métodos, y habrá que darles una implementación.

Muy habitual es lanzar una excepción, o simplemente no hacer nada.

Pero, al igual que vimos en algún ejemplo en el principio de sustitución de Liskov, esto es peligroso. Si lanzamos una excepción, es más que probable que el módulo que define esa interfaz use el método en algún momento, y esto hará fallar nuestro programa.

El resto de implementaciones “por defecto” que podamos dar, pueden generar efectos secundarios que no esperemos, y a los que sólo podemos responder conociendo el código fuente del módulo en cuestión, cosa que no nos interesa.

¿Cómo detectar que estamos violando el Principio de segregación de interfaces?

Como comentaba en los párrafos anteriores, si al implementar una interfaz ves que uno o varios de los métodos no tienen sentido y te hace falta dejarlos vacíos o lanzar excepciones, es muy probable que estés violando este principio.

Si la interfaz forma parte de tu código, divídela en varias interfaces que definan comportamientos más específicos.

Recuerda que no pasa nada porque una clase ahora necesite implementar varias interfaces. El punto importante es que use todos los métodos definidos por esas interfaces.

Ejemplo

Imagina que tienes una tienda de CDs de música, y que tienes modelados tus productos de esta manera:

interface Product {
    val name: String
    val stock: Int
    val numberOfDisks: Int
    val releaseDate: Int
}

class CD : Product {
    ...
}

El producto tiene una serie de propiedades que nuestra clase CD sobrescribirá de algún modo.

Pero ahora has decidido ampliar mercado, y empezar a vender DVDs también.

El problema es que para los DVDs necesitas almacenar también la clasificación por edades, porque tienes que asegurarte de que no vendes películas no adecuadas según la edad del cliente.

Lo más directo sería simplemente añadir la nueva propiedad a la interfaz:

interface Product {
    ...
    val recommendedAge: Int
}

¿Qué ocurre ahora con los CDs? Que se ven obligados a implementar recommendedAge, pero no van a saber qué hacer con ello, así que lanzarán una excepción:

class CD : Product {
    ...
    override val recommendedAge: Int
        get() = throw UnsupportedOperationException()
}

Con todos los problemas asociados que hemos visto antes.

Además, se forma una dependencia muy fea, en la que cada vez que añadimos algo a Product, nos vemos obligados a modificar CD con cosas que no necesita.

Podríamos hacer algo tal que así:

interface DVD : Product {
    val recommendedAge: Int
}

Y hacer que nuestras clases extiendan de aquí.

Esto solucionaría el problema a corto plazo, pero hay algunas cosas que pueden seguir sin funcionar demasiado bien.

Por ejemplo, si hay otro producto que necesite categorización por edades, necesitaremos repetir parte de esta interfaz.

Además, esto no nos permitiría realizar operaciones comunes a productos que tengan esta característica.

La alternativa es segregar las interfaces, y que cada clase utilice las que necesite. Tendríamos por tanto una interfaz nueva:

interface AgeAware {
    val recommendedAge: Int
}

Y ahora nuestra clase DVD implementará las dos interfaces:

class CD : Product {
    ...
}

class DVD : Product, AgeAware {
    ...
}

La ventaja de esta solución es que ahora podemos tener código AgeAware, y todas las clases que implementen esta interfaz podrían participar en código común.

Imagina que no vendes sólo productos, sino también actividades, que necesitarían una interfaz diferente.

Estas actividades también podrían implementar la interfaz AgeAware, y podríamos tener código como el siguiente, independientemente del tipo de producto o servicio que vendamos:

fun checkUserCanBuy(user: User, ageAware: AgeAware) 
    = user.age >= ageAware.recommendedAge

¿Qué hacer con código antiguo?

Si ya tienes código que utiliza fat interfaces, la solución puede ser utilizar el patrón de diseño “Adapter”. El patrón Adapter nos permite convertir unas interfaces en otras, por lo que puedes usar adaptadores que conviertan la interfaz antigua en las nuevas.

Conclusión

El principio de segregación de interfaces nos ayuda a no obligar a ninguna clase a implementar métodos que no utiliza. Esto nos evitará problemas que nos pueden llevar a errores inesperados y a dependencias no deseadas. Además nos ayuda a reutilizar código de forma más inteligente.

Y si lo prefieres, puedes descargarte lo que estamos viendo aquí en esta guía de Principios SOLID de forma gratuita.

En el siguiente artículo acabamos finalmente con las reglas SOLID, hablando de uno de los principios más interesantes: el Principio de Inversión de Dependencias.

Comenta cualquier duda o sugerencia en la sección de comentarios.

Quizá también te interese…

Cómo modularizar una Aplicación Android

Cómo modularizar una Aplicación Android

Cómo modularizar una aplicación Android En este artículo, vamos a hablar sobre la modularización de aplicaciones Android. La modularización es un proceso que consiste en dividir una aplicación en varios módulos, para facilitar su mantenimiento y escalabilidad. La...

Las reglas FIRST de los tests

Las reglas FIRST de los tests

Las reglas FIRST son un conjunto de principios que se utilizan para diseñar y escribir tests de software de manera efectiva. Las siglas FIRST significan: F - Fast: Un test debe ser rápido de ejecutar. I - Independent: Un test debe ser independiente de otros tests y...

¿Qué son los dobles de test?

¿Qué son los dobles de test?

Los dobles de prueba (también conocidos como "doubles" o "fakes") son herramientas comunes en la programación y en particular en el testing de software. Se utilizan para simular el comportamiento de una dependencia de una aplicación en un entorno de pruebas, sin tener...

12 Comentarios

  1. adordth

    Excelente blog. Me agradó la explicación de los cuatro principios. Gran trabajo!

    Responder
    • Antonio Leiva

      Gracias! Para este jueves público el quinto, no te lo pierdas!

      Responder
  2. Jhonny

    buen blog, tengo algunas dudas y si es posible el escribirte?

    Responder
    • Antonio Leiva

      Claro! Para eso está la sección de contacto. Ahí tienes un formulario para escribirme.

      Responder
  3. Renzo D. “ozzner” Santillán Ch.

    Hola Antonio, está bastante buena la serie de principios solid y también espero ansioso los temas que mencionaste a lo largo de los post (Inmutabilidad y ahora patrón adapter). =)

    Responder
  4. Sebastian

    Hola Antonio, en el código del ejemplo tienes:

    public interface DVD extends Product {
    int getRecommendedAge();
    }

    Está bien que una interfaz herede de otra? Generalmente lo que se hace es implements, pero se puede hacerlo con extends también? Muchas gracias!

    Responder
    • Antonio Leiva

      Si una interfaz implementa otra, Java te obliga a poner extends, no implements. Cosas de Java 🤷‍♂️

      Responder
  5. Mikel

    Muy buenas Antonio y enhorabuena por tu blog y tus artículos. Creo que llego varios años tarde, pero me he decidido a escribir por si sigues por ahí.

    Sobre este principio… ¿la idea que planteaba Robert Martin no es al revés?

    Quiero decir, el problema no lo plantea desde la perspectiva de las clases que implementan la interfaz, sino desde las que lo usan. Martin lo plantea como el problema de que usar una interfaz que tiene métodos que tú no necesitas.

    Responder
    • Antonio Leiva

      Sigo, sigo! Y tienes toda la razón, a ver si le echo un ojo y actualizo el texto. Muchas gracias!

      Responder
  6. Cristian

    Bueno si el ultimo comentario fue en 2019, que decir de este, me gusto la explicación, no da vueltas, da un ejemplo.

    Muchas gracias por compartir conocimiento. 🙂

    Responder
    • Antonio Leiva

      Gracias! Además lo he reescrito desde entonces, así que algunos comentarios pueden estar obsoletos. La gente hoy en día ya no comenta en blogs 😄, y no porque no tengan visitas… Estos artículos son de los más visitados del blog.

      Responder

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de DevExpert, S.L.U., con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

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

Acepto la política de privacidad *