El principio de segregación de interfaces (ISP) se ocupa de la forma en que los clientes acceden a la funcionalidad desarrollada en otra clase. Establece que los clientes no deben ser obligados a depender de funcionalidad que no utilizan.
Seguir este principio tiene varias ventajas. Por un lado, protege tus objetos de depender de cosas que no necesitan. Eliminar dependencias no deseadas y desenredar nuestras abstracciones son algunos de nuestros principales objetivos al limpiar un diseño de software. Otro beneficio es que te permite identificar las abstracciones correctas y construir paquetes de comportamiento cohesivo.
Históricamente, otra razón para seguir el ISP era la ventaja de recompilar menos piezas de código cuando las clases referenciadas cambiaban. Con el hardware actual, esto no es un gran problema en la mayoría de los casos. El principal beneficio es la creación de abstracciones consistentes y granulares.
En resumen, el ISP es una herramienta que te guía en la creación de interfaces y abstracciones bien definidas.
Depender solo de lo que necesitas
Robert Martin dijo:
Depender de algo que no necesitas puede causarte problemas que no esperabas _Robert Martin
Aplicar esta idea al diseño de software nos lleva a una conclusión obvia: un objeto no debería depender de comportamiento que no necesita. Supongamos que tenemos una clase grande con mucha funcionalidad. La llamaremos BigClass. Ahora, hay 3 otros tipos de objetos (ClientOne, ClientTwo y ClientThree) que usan funcionalidad de BigClass.
Las tres clases usan solo una porción de la funcionalidad de la clase base, pero como la referencian, dependen implícitamente de un montón de funcionalidad que no usan. Podemos refactorizar este diseño para seguir el ISP creando 3 interfaces más pequeñas que abarcuen solo el comportamiento usado por cada uno de los 3 clientes. Esto lleva a una estructura que se ve así:
Ahora, cada cliente depende solo de la funcionalidad que necesita. Podrías pensar que sería mejor si en lugar de la BigClass tuviéramos clases más pequeñas que implementen cada una de las 3 nuevas interfaces, y tienes razón. En realidad, esto no siempre es posible, y a veces una clase grande puede ser extremadamente difícil de dividir en otras más pequeñas.
El ISP reconoce que a veces estos objetos son necesarios. En su lugar, trata de proteger a los clientes asegurando que no sepan sobre todo el comportamiento en la clase. Paquetes de comportamiento relacionado usado por un solo cliente deben ser divididos en interfaces más pequeñas y más cohesivas.
Cuidado: los padres o ancestros
Tus clases muestran problemas respecto al ISP tan pronto como comienzan a engordarse y acomodar comportamientos que no están conceptualmente relacionados.
Hay muchas fuentes para este engrosamiento, una causa común es el mal uso de la herencia. Esto usualmente toma la forma de crear subclases de un objeto en la clase padre para dar a una de las subclases un comportamiento especial.
Supón que eres un científico loco malvado trabajando en tu último invento: robots de alarma. Quieres agregar a tu ejército del mal un tipo de robot que patrullará y notificará al sistema de alarma si encuentra algo extraño.
Ya tienes una clase EmergencyNotifier en la cual puedes registrar fuentes de alarma del tipo Alarm, y ahora quieres registrar tus robots de alarma.
¿Una solución? hacer que Robot herede de alarm, de esa manera puedes registrar AlarmRobot con el EmergencyNotifier. Piensas que otros tipos de Robot podrían beneficiarse de ser registrables en el futuro, así que procedes y haces que Robot sea subclase de Alarm.
¡Genial, ahora puedes registrar los robots de alarma y las notificaciones funcionan como se esperaba!
Esto, como imaginaste, no es una buena solución. La interfaz original de Robot ahora está contaminada con comportamiento extraño. Convenientemente, puedes registrar Robots en el notificador de emergencias, pero al heredar de alarm tu abstracción original ahora está rota. Este cambio obliga a todos los clientes que usan estas clases a depender de comportamiento que no necesitan.
Hay muchas soluciones para hacer que este diseño cumpla con el ISP, pero dos son obvias y fáciles de implementar:
- Si tu lenguaje ofrece la opción de usar herencia múltiple, entonces puedes hacer que AlarmRobot herede tanto de Robot como de Alarm. No soy gran fanático de la herencia múltiple, así que personalmente evitaría esta solución.
- Crear una interfaz que encarne una abstracción de Alarm y luego hacer que todos los objetos tipo-Alarm en tu sistema la implementen. De esta manera el comportamiento que AlarmRobot necesita está segregado en múltiples interfaces.
Después de aplicar la segunda solución nuestro sistema se verá así:
Protege a tus clientes
La lección principal de este artículo es que no debes forzar a los clientes a depender de comportamiento que no necesitan. En un mundo ideal, cada interfaz/abstracción en tu código encarnaría el comportamiento completo que sus usuarios necesitan.
Esto es difícil de lograr en la realidad. Por diferentes razones, probablemente terminarás con un par de objetos grandes que hacen muchas cosas. Este principio reconoce que estas clases existen y te insta a crear abstracciones relevantes que encarnen pedazos de comportamiento relacionado implementado en esas clases.
Para aplicar efectivamente el ISP necesitarás prestar atención al crecimiento de tu sistema y asegurar que tus interfaces sean consistentes y eficientes.
Qué hacer a continuación
- Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a personas que podrían encontrar útil esta información.
- Lee el artículo de Bob Martin sobre este tema: ISP.
- Hay más información sobre el OCP en el capítulo 10 de Clean Architecture.
- Envíame un email con preguntas, comentarios o sugerencias (está en la página Autor).