SOLID y GRASP
Los cinco principios SOLID y las nueve técnicas GRASP de asignación de responsabilidades, con el acoplamiento y la cohesión como criterio de fondo.
Single Responsibility: una sola razón para cambiar
El principio de responsabilidad única dice que una clase debe tener una sola responsabilidad, o dicho con más precisión, una sola razón para cambiar. Si dos motivos distintos pueden obligar a tocar la misma clase, es que dentro hay dos clases mal separadas.
El síntoma clásico es la superclase que lo hace todo: una clase Intento que corrige respuestas, calcula estadísticas, decide el siguiente repaso y además se guarda ella misma en base de datos. Cuando cambie el formato del examen habrá que tocarla, cuando cambie la metodología de repaso también, y cuando cambie la base de datos otra vez.
La pregunta útil no es «¿cuántas cosas hace esta clase?», sino «¿quién puede pedirme que la cambie?». Si la respuesta son dos áreas de negocio distintas, sobra una clase.
Para el examen
Qué exige: una clase debe tener una sola razón para cambiar
Su letra: la S de SOLID
Formulación célebre: «one, and only one, reason to change»
Open/Closed: abierto a extensión, cerrado a modificación
El principio de abierto y cerrado dice que debe ser posible extender el comportamiento de una clase sin modificar su código. Se debe poder añadir lo nuevo sin volver a tocar, ni volver a probar, lo que ya funcionaba.
La forma habitual de conseguirlo es la herencia o la implementación de una interfaz: se hereda de la clase existente y se añaden los cambios ahí, en lugar de abrir el original. La excepción razonable es que la clase original estuviera mal diseñada, en cuyo caso lo que toca es arreglarla.
El indicador de que se está incumpliendo es el bloque de condicionales que crece cada vez que aparece un caso nuevo. Si añadir un tipo de test obliga a añadir una rama más a un condicional que ya tiene ocho, ese código está abierto a modificación y cerrado a extensión, justo al revés.
Para el examen
Qué exige: abierto a extensión y cerrado a modificación
Qué permite: añadir lo nuevo sin tocar lo ya probado
Síntoma de incumplirlo: el condicional que crece con cada caso nuevo
Liskov: las hijas deben poder sustituir a la madre
El principio de sustitución de Liskov exige que un objeto de una subclase pueda ocupar el sitio de uno de su clase base sin que el programa que lo usa deje de comportarse correctamente. No basta con que compile: tiene que seguir funcionando y dando el resultado esperado.
En la práctica impone tres condiciones a la subclase: no puede exigir más que su madre para aceptar una llamada, no puede prometer menos al devolver el resultado, y no puede romper las reglas que la madre garantizaba siempre. Un método heredado que de pronto lanza una excepción donde antes no la lanzaba nunca incumple Liskov.
Liskov es la condición que hace que el polimorfismo sea de fiar. Si una subclase no es sustituible, el cliente que programa contra el tipo abstracto acaba teniendo que preguntar de qué clase es cada objeto, y eso destruye el polimorfismo.
Cumplirlo obliga a jerarquías robustas y bien pensadas. Es más restrictivo de lo que parece: que dos clases se parezcan no autoriza a colgar una de la otra si el comportamiento no se conserva.
Para el examen
Qué exige: que un objeto de la subclase pueda sustituir a uno de la superclase sin romper el programa
Sus tres reglas: no exigir más, no prometer menos y no violar invariantes
Por qué importa: es lo que hace fiable el polimorfismo
Interface Segregation: interfaces pequeñas y a medida
El principio de segregación de interfaces dice que hay que ofrecer interfaces finas, específicas para cada cliente, y que ningún cliente debe verse obligado a depender de operaciones que no utiliza.
El caso típico es un servicio grande con diez métodos que usan tres subsistemas distintos, cada uno de los cuales solo necesita tres o cuatro. La solución no es partir el servicio en tres implementaciones, sino declarar tres interfaces pequeñas y hacer que la misma clase las implemente todas. Cada subsistema depende solo de la suya, que es como asomarse al servicio por una ventana estrecha.
Segregar interfaces no obliga a partir la implementación: una sola clase puede implementar varias interfaces a la vez. Lo que se parte es lo que ve cada cliente, no lo que hay detrás.
Para el examen
Qué exige: interfaces finas, a medida de cada cliente
Principio: nadie debe depender de operaciones que no usa
Qué se parte: las interfaces, no la implementación
Dependency Inversion: depender de abstracciones
El principio de inversión de dependencias dice que hay que depender de abstracciones y no de concreciones. El papel de abstracción lo hacen las interfaces, y las clases concretas son sus implementaciones; el código de negocio debe escribirse contra las primeras.
La razón es de estabilidad: las interfaces cambian poco y las implementaciones cambian mucho. Si el resto del sistema depende de algo que casi no varía, casi nada tiene que moverse cuando cambie la implementación.
Está directamente relacionado con la inyección de dependencias: en lugar de que cada clase se fabrique por su cuenta lo que necesita, es el contenedor quien le entrega ya construida la implementación concreta y se la asocia a un atributo declarado con el tipo de la interfaz. La clase que la recibe no llega a enterarse de cuál le han dado. Spring es el marco de trabajo que popularizó esta técnica en Java.
// Mal: el servicio se ata a una implementación concreta.
public class ServicioDeRepasos {
private RepositorioIntentosSql repo = new RepositorioIntentosSql();
}
// Bien: depende de la abstracción y se la inyectan.
public class ServicioDeRepasos {
private final RepositorioIntentos repo; // interfaz
public ServicioDeRepasos(RepositorioIntentos repo) {
this.repo = repo;
}
}Para el examen
Qué exige: depender de abstracciones y no de concreciones
Por qué: las interfaces cambian poco
Cómo se materializa: con inyección de dependencias
Quién la popularizó en Java: Spring
GRASP: cómo se reparten las responsabilidades
GRASP son las siglas de General Responsibility Assignment Software Patterns. No son patrones de código como los de la banda de los cuatro, sino criterios para decidir qué clase se hace cargo de cada cosa, que es la decisión que más condiciona un diseño orientado a objetos.
| Técnica | Criterio que propone |
|---|---|
| Information Expert | Asignar la responsabilidad a la clase que ya tiene la información necesaria para realizar esa tarea |
| Creator | Que cree un objeto la clase que lo contiene, lo agrega o dispone de sus datos de creación |
| Controller | Un objeto no de interfaz de usuario que recibe los eventos del sistema y los reparte |
| Indirection | Meter un intermediario entre dos elementos para que no se conozcan directamente |
| Low Coupling | Que cada clase conozca a las menos posibles |
| High Cohesion | Que cada clase tenga un cometido claro y compacto |
| Polymorphism | Repartir el comportamiento variable entre subtipos en lugar de preguntar por el tipo |
| Protected Variations | Envolver tras una interfaz estable lo que se prevé que va a cambiar |
| Pure Fabrication | Inventar una clase que no existe en el negocio cuando hace falta para no ensuciar las que sí |
La más preguntada es Information Expert, y también la más natural: si la clase Intento es la que guarda los aciertos y el total, el cálculo del porcentaje le corresponde a ella y no a un servicio externo que le pida los datos.
Para el examen
Qué son: criterios de asignación de responsabilidades, no patrones de código
Information Expert: la tarea va a quien tiene los datos
Los otros más preguntados: Creator, Controller, Low Coupling y High Cohesion
Bajo acoplamiento y alta cohesión
El acoplamiento mide cuánto depende una clase de otras. Bajo acoplamiento significa que cada clase conoce a las menos posibles, así que un cambio en una no se propaga por media aplicación.
La cohesión mide cuánto tienen que ver entre sí las cosas que hace una clase. Alta cohesión significa que todo lo que hay dentro sirve al mismo cometido, y es lo que evita las clases cajón de sastre.
Las dos van juntas y en la misma dirección: se busca acoplamiento bajo y cohesión alta. Cuando se abandonan las dos aparece el llamado código espagueti, con un flujo complejo e incomprensible en el que nadie sabe qué toca qué.
Conviene recordar el motivo de fondo: las técnicas de diseño están, casi siempre, para poder hacer un mejor mantenimiento. Un sistema con bajo acoplamiento y alta cohesión no se ejecuta más rápido; se cambia más barato, que es donde se va el dinero de un proyecto a lo largo de su vida.
Para el examen
Acoplamiento: cuanto más bajo, mejor: conocer a las menos clases posibles
Cohesión: cuanto más alta, mejor: un cometido claro
Qué produce su ausencia: código espagueti
Fin último del diseño: abaratar el mantenimiento