Acoplamiento temporal
Creo que todos habremos leído más de un artículo o libro en el que, mencionando las buenas prácticas que debemos seguir a la hora de diseñar e implementar nuestras aplicaciones, se hace especial hincapié en dos conceptos:
- Alta cohesión (high cohesion): sin ánimo de profundizar, la alta cohesión se consigue cuando una clase hace una labor bien definida, y no múltiples tareas poco relacionadas. Una medida de alta cohesión podría ser el uso que los diferentes métodos de una clase hacen de las variables de instancia, si todos los métodos utilizan todas las variables de instancia la cohesión es alta. A nivel de módulo voy a poner un contraejemplo bastante significativo, la clase
java.util
, que contiene tanto las Java Collections como clases para manejar fechas y demás. Curioso que algo tan definido como las Collections no tenga su propio paquete, pero evidentemente por motivos históricos y compatibilidad hacia atrás se ha tenido que quedar así
- Bajo acoplamiento (loose coupling): dos clases están muy acopladas cuando su interacción se basa en detalles de implementacion y no en abstracciones, por lo que un cambio en la implementación de una de ellas forzará la modificación de la otra. Para conseguir bajo acoplamiento debemos seguir principios tan importantes como “programa sobre interfaces y no sobre implementaciones” o los principios SOLID (entre los que destaca el principio de inyección de dependencias, del que ya hablamos)
Siempre me ha llamado la atención que no se preste atención al “acoplamiento temporal” (temporal coupling), quizás porque no es tan importante. De hecho no he encontrado demasiadas referencias en castellano, así que creo que merece la pena repasar en qué consiste y cómo evitarlo cuando sea posible.
Diseñando una API
Cuando creamos una API, es de recibo hacerlo cuidadosamente de forma que sea clara para los clientes que la vayan a utilizar, que no exponga detalles internos de implementación (para conseguir el bajo acoplamiento), que no abarque más de lo necesario (alta cohesión), y a ser posible que sea intuitiva en su uso. La elección de nombres aquí es muy importante, y en ningún caso nuestros métodos deberían hacer más de lo que el usuario espera de ellos, lo que se conoce como “Principio de la mínima sorpresa” (Principle of Least Surprise).
Vamos a crear una API que prepare un plato para un restaurante. Una primera versión podría ser:
Se trata de una interfaz, así que creemos una implementación sencilla:
La clase Ingredient
es trivial:
¿Podríamos decir que este diseño cumple los principios reseñados más arriba?
- Alta cohesión: los diferentes métodos expuestos por
Dish
están claramente relacionados entre ellos, ya que forman parte del proceso de preparación de un plato - Bajo acoplamiento: los clientes de esta clase la utilizarán únicamente mediante su interfaz, y necesitarán una dependencia adicional (la clase
Ingredient
). Siempre que respetemos la API no deberíamos preocuparnos por los detalles de implementación - Principio de la mínima sorpresa: bueno, no parece que la invocación a
serve
haga nada más que servir el plato (no limpia el horno que se puede haber utilizado para prepararlo, por ejemplo :) ). Lo mismo con los demás métodos.
Parece que no está mal. Veamos cómo utilizaríamos la API:
Si invocamos el método Chef.cookPaella
, la salida sería:
Qué es el acoplamiento temporal
Vamos a realizar una modificación muy pequeña en la clase Chef
:
Hemos comentado el método mix
, ya que consideramos que no es necesario mezclar los ingredientes antes de cocinarlos. La salida de la aplicación, en este caso, es:
¡La excepción nos indica que los ingredientes no están mezclados! Es decir, los métodos ‘mix’ y ‘cook’ están acoplados temporalmente, de forma que es necesario invocarlos en un orden determinado. En esto exactamente consiste el acoplamiento temporal. De hecho, no es el único que existe en la clase Dish
, los métodos cook
y serve
también están acoplados temporalmente, y lo mismo ocurre con addIngredient
y mix
. En general, la API completa requiere que los métodos sean invocados siguiendo un orden determinado, o no ejecutará de forma correcta su tarea.
Como desarrolladores, deberíamos evitar en la medida de lo posible llegar a este tipo de condiciones de uso, ya que dificulta el desarrollo a terceras partes (los clientes de nuestra API). En el ejemplo está muy claro lo que ocurre, pero en otros casos se puede dar lugar a bugs escondidos y difíciles de seguir.
Medidas para evitarlo
Primero de todo, no siempre es posible evitar completamente el acoplamiento temporal, así que, en caso de que no quede otro remedio la solución es hacerlo patente mediante excepciones explícitas en caso romperlo. Por tanto, la primera medida sería mejorar los mensajes de nuestras excepciones. En nuestro ejemplo, el método cook
podría quedar así:
Un cambio tan sencillo dejará claro al usuario cómo ha de proceder en caso de error.
Pero antes de llegar a este punto debemos intentar eliminar los acoplamientos existentes, obligando al usuario a utilizar nuestra API de una forma determinada (= más rígida). En ocasiones mayor rigidez no signfica peor diseño. En el caso concreto de la API Dish
, vamos a obligar al usuario a pasar en el constructor la lista de ingredientes que componen el plato, veamos cómo quedaría:
Hemos simplificado la interfaz, de forma que solo contiene los métodos cook
y serve
. Esto maximiza la flexibilidad de las implementaciones, ¡tanto que ni siquiera estamos obligados a utilizar instancias de Ingredient
al uso! El constructor de la implementación obliga a los clientes a pasar una lista de ingredientes, y una vez almacenados localmente los mezcla sin necesidad de que lo hagan los usuarios. ¿No es más claro el uso ahora?
La salida sería:
En este punto la única restricción es que los métodos cook
y serve
deben ejecutarse ambos y por este orden (siempre que nos queramos comer el plato, claro, siempre podemos cocinarlo sin más…). Podríamos rizar el rizo y fusionarlos en un nuevo método cookAndServe
como método único en la interfaz, pero creo que ya han quedado bastante claros los conceptos (cómo dirían algunos, lo dejo como ejercicio :)). Por otro lado, un uso incorrecto de la API quedará expuesto inmediatamente por las excepciones lanzadas.
Bonus: DSL’s
En los últimos tiempos se está extendiendo mucho el uso de DSL’s (Domain Specific Language), que a grandes rasgos son fluent API’s (basadas en el patrón Builder), y permiten diseñar soluciones a problemas específicos de forma muy legible. Por ejemplo, el framework de integración Apache Camel nos permite crear una ruta entre dos puntos con una transformación en medio tal que así:
No creo que haga falta entrar en muchos detalles para entender lo que haría este pequeño fragmento de código. Este tipo de fluent API contiene decenas de métodos, existiendo en ocasiones acoplamiento temporal.
Lo importante si diseñamos DLS’s es, como hemos comentado más arriba, exponer el uso incorrecto con mensajes lo suficientemente claros (refiriendo incluso a links con documentación, etc).