Principio de Segregación de Interfaces para Abuelas

Hace un par de semanas escribía sobre lo que realmente significa el principio de inversión de independencias con el objetivo de intentar profundizar más allá de la implementación típica y poder ver en qué ideas se basa y dónde más podemos aplicar esas ideas.

Siguiendo con esa línea, en este post vamos a aprovechar para analizar el trasfondo del principio de segregación de interfaces (PDF), la I de SOLID, que seguramente sea uno de los principios de SOLID menos citados y menos tenidos en cuenta de los cinco.

abuela-preocupada

Segregación de interfaces para abuelas

Imagina que tu abuela quiere contratar una línea de teléfono fijo para hablar con sus amigas. Cuando va a hablar con la operadora de turno, encuentra una oferta en la que, por el mismo precio que una línea, le ofrecen un pack que incluye la línea de teléfono, internet y televisión por cable.

Aunque tu abuela sólo va a usar la línea, ya que le cuesta lo mismo una cosa que otra, decide contratar el pack. Total, así si un día quiere aprender eso del internet o ver algún programa en un canal de los raros, puede hacerlo.

Unas semanas más tarde, un día descubre que no puede llamar por teléfono a su amiga Puri para felicitarle por su cumpleaños. Parece ser que la línea de teléfono de su pack utiliza algo que se llama voz sobre ipé o algo así, y cuando no funciona el aparato rúter, no se puede llamar. Por suerte un simpático técnico de la operadora le cambia el rúter y le soluciona el problema, pero ya no consigue felicitar a tiempo a Puri en su cumpleaños (y a ciertas edades no sé sabe cuántas oportunidades quedan para ello).

Otro día, tampoco puede llamar a Dora para acercarse a visitarla. Una chica muy agradable de la operadora le explica que es que tienen que actualizar remotamente el finguar de su rúter para darle más megas del internet, y que mientras lo hacen no podrá conectarse y su voz sobre ipé no funcionará.

Estos problemas que ha tenido tu (hipotética) abuela, vienen motivados por violar el principio de segregación de interfaces.

Tu abuela sólo quería hablar por teléfono, pero ha adquirido una dependencia sobre algo mucho mayor, el pack internet+llamadas+televisión, por lo que cuando algo de ese pack falla, aunque sea algo que ella no utiliza para nada, se ve afectada por ello.

El enunciado formal

En el artículo que enlazaba antes, Robert C. Martin explica el principio de segregación de interfaces en detalle, pero no sé si es porque el artículo ha resistido mal el paso del tiempo o ese día el autor estaba poco inspirado, pero la argumentación resulta a veces un poco confusa y, el diseño final al que llega es, al menos desde mi punto de vista, bastante discutible.

Sin embargo, la idea de base es sencilla y bastante razonable: si tienes un componente “grande” que es consumido por varios clientes, y cada cliente utiliza sólo ciertas características del componente “grande”, ese cliente debería depender sólo de esas características. El enunciado formal es:

Los clientes no deben verse obligados a depender de interfaces que no utilizan.

Que traducido a lenguaje de abuelas sería:

No contrates cosas que no necesitas.

Es importante tener en cuenta que cuando hablamos de interfaces, no estamos (necesariamente) hablando de interfaces como los de Java o C#, sino del API que expone un objeto concreto. Tal vez un término más apropiado sería contratos, que es el que usaré en el resto de post para distinguir entre estos interface/APIs, y los interfaces de C#/Java.

El trasfondo de todo esto

¿Qué es lo malo de tener contratos grandes? A fin de cuentas, puede ser que estemos agrupando funcionalidad que conceptualmente esté relacionada y, a priori, tendría sentido mantenerla unida en aras de aumentar la cohesión. Además, puestos a incluir a una dependencia, mejor tener una que hace más cosas por si acaso en algún momento me hacen falta, ¿no?

El problema, como casi siempre, es el acoplamiento. La típica disyuntiva entre cohesión y acoplamiento que se produce al desarrollar software.

Si tenemos un contrato grande pero cada cliente utiliza una parte diferente, estamos acoplando implícitamente unos clientes con otros a través de ese contrato.

En el ejemplo de la abuela, el mismo contrato (el pack completo) sirve a clientes que sólo quieren llamar, como ella, y a otros que quieren navegar por internet. Para mejorar el servicio de los clientes que usan internet (darles más megas) hace falta cambiar la implementación (actualizar el finguar del rúter) y eso implica pérdida de servicio para clientes que, en principio, no tendrían motivos para verse afectados por ello.

Si lo llevamos a código, tendríamos algo como esto:

public interface IMegaPack {
    void MakePhoneCall();
    void ConnectToInternet();
    void WatchCableTV();
}

Es un único interfaz que expone toda la funcionalidad, por lo que obligamos a todos los clientes depender del interfaz completo. La solución es sencilla, podemos partir el interfaz en trozos más pequeños:

public interface IVoice
{
    void MakePhoneCall()
}

public interface IInternet
{
    void ConnectToInternet();
}

public interface ICableTV
{
    void WatchCableTV();
}

public interface IMegaPack : IVoice, IInternet, ICableTV
{
}

Es importante tener en cuenta que partir el interfaz no quiere decir que partamos su implementación. Técnicamente podríamos mantener un único objeto que implementa todos los interfaces y cada cliente lo vería sólo como la parte que le interesa.

Cuando se aplica esta idea de un único objeto que implementa varios interfaces específicos para cada caso de uso, a los interfaces se les suele llamar role interface.

Aunque la situación mejora porque los clientes no quedan acoplados a través del contrato, sí que siguen acoplados a través de la implementación (al final es el mismo objeto), por lo que la solución no es todo lo buena que a uno le gustaría. Dependiendo del caso, esto será más o menos problemático y solucionarlo será más o menos fácil (a veces existe un acoplamiento real entre las funcionalidades y no es sencillo evitarlo).

Otro problema adicional de depender de objetos grandes, es que cuanto mayor es el tamaño de un objeto más probable es que tenga más dependencias, por lo que los clientes estarán asumiendo transitivamente un número mayor de dependencias.

Al crear interfaces más pequeños, tenemos la ventaja adicional de que, si es necesario crear nuevas implementaciones del interfaz para un caso de uso concreto, no necesitaremos implementar también el resto de métodos que no están relacionados con ese caso de uso.

Llevando al extremo la segregación de interfaces, acabaríamos con intefaces degenerados que tienen un único método. Es razonable. En muchos lenguajes a esas cosas se les llaman funciones y a veces son la mejor solución, como decía John Carmack:

A veces, la implementación elegante es sólo una función. No un método. No una clase. No un framework. Sólo una función.

De contratos a conceptos

Si ampliamos un poco nuestros horizontes y dejamos de pensar en contratos como interfaces u objetos, veremos que hay ideas del principio de segregación se puede aplicar en más ámbitos.

Por ejemplo, es fácil abusar de un gestor de paquetes para añadir dependencias sobre un paquete que hace 1000 cosas cuando sólo necesitas 2.

Eso hará que tengamos que asumir como propias dependencias de ese paquete que, en realidad, no están relacionadas con las cosas que utilizamos.

También puede ocurrir con cosas que cuesta considerar interfaces/contratos.

Si tenemos un método que necesita obtener las ventas de un producto, ¿es mejor pasarle el objecto Product o sólo el id? Ya hablamos de eso hace tiempo pero si le pasas el objeto Product y sólo necesita el id, se introducirá una dependencia sobre un contrato mayor (toda la clase Product) por lo que quedará acoplado al resto de clientes de esa clase.

Aplicando el principio de segregación de interfaces, podríamos pensar que si ese método sólo necesita poder obtener un id, no debería quedar acoplado al contrato completo de Product, con su nombre, su precio y unas cuantas decenas de propiedades más.

Muchas veces al pensar en contratos e interfaces nos vamos inmediatamente a pensar en servicios, pero pasa lo mismo con conceptos más “tangibles” dentro de nuestras aplicaciones. Si parte de nuestro sistema sólo necesita ciertos datos y operaciones de un cliente, y otra parte del sistema necesita datos y operaciones diferentes, aunque en la vida real sea la misma entidad (la empresa cliente), seguramente sea buena idea separarlo en dos contratos distintos y, posiblemente, en dos implementaciones distintas también.

Conclusiones

En cuanto te alejas un poco del código, el principio de segregación de interfaces consiste, básicamente, en simplificar al máximo las dependencias entre componentes, haciéndolas lo más específicas y focalizadas posibles.

La principal ventaja de esto es que vamos a conseguir componentes menos acoplados ya que dependerán de roles más específicos, y eso nos facilitará evolucionarlos por separado y aumentará la flexibilidad del sistema.

Debemos tener en cuenta que si al final esos roles son implementados por un único objeto, aunque hayamos independizado los contratos seguiremos teniendo acoplamiento a través de la implementación. Si optamos por implementaciones separadas para cara rol, el riesgo que asumimos es acabar con un código poco cohesionado más complicado de enteder y mantener.

Como siempre, cada caso merece una consideración aparte, pero si aplicas el sentido cómun y comprendes en qué se fundamentan los principios que estás aplicando, podrás tomar mejores decisiones.

Fotografía por Natalia Rivera con licencia CC BY 2.0

Un comentario en “Principio de Segregación de Interfaces para Abuelas

  1. Cuando tengo que pensar en este principio, siempre me acuerdo del infame MembershipProvider de ASP.NET. Un infierno de métodos que estás obligado a implementar cuando creas un provider personalizado. Aunque bueno, el código lleno de NotImplementedException, queda muy bonico ;-)

    Buen artículo, como siempre.

Comentarios cerrados.