Raciones de diseño de software
Raciones de diseño de software
Fran Iglesias
Buy on Leanpub

1 Introducción

No tengo una buena definición de los que es Diseño de Software o de Arquitectura. De hecho, no me gusta la palabra Arquitectura al hablar de software, porque creo que la metáfora está mal aplicada. Otra cosa sería Arquitectura de Sistemas. Me explico.

Yo veo la Arquitectura como un saber que se ocupa de cómo se sostienen las cosas. Es decir, cómo los componentes colaboran entre sí para distribuir y gestionar las fuerzas que intervienen en que las construcciones se sostengan y perduren, permitiendo a las personas desarrollar su vida en ellas.

Sin embargo, lo que llamamos Arquitectura en Software se ocupa más bien de cómo los distintos componentes colaboran comunicándose. La arquitectura de software define las reglas que dicen qué componentes pueden hablar entre ellos y cuáles no, cuál es la dirección de la comunicación y quién se encarga de qué.

Por esa razón, personalmente me gusta más el término Diseño de Software, más que Arquitectura. Porque creo que, dentro de sus limitaciones, captura mejor la idea de lo que representa para mí.

En este libro hablo de Diseño de Software. De principios, patrones y prácticas, por ser más preciso. La mayor parte de su contenido nació como hilos en Twitter. Durante una temporada usé este medio para exponer algunas ideas. Para mi sorpresa, esta iniciativa tuvo buena acogida y cierto éxito. Hasta el punto de que hay quien me reconoce por los hilos de Twitter, lo cual no sé si será bueno o malo, pero es lo que hay.

Obviamente, con esos orígenes no puedes esperar que este libro sea un manual de referencia sobre diseño de software. Es más bien una colección caótica de ideas que he ido aprendiendo o elaborando a lo largo de los años como desarrollador. No pretendo que sea una obra de consulta, ni mucho menos. En todo caso, puede ser una introducción ligera y no demasiado rigurosa a una serie de temas que como desarrolladoras deberíamos considerar.

Pienso que hay una diferencia entre programar y desarrollar. Diría que programar es el hecho de describir un proceso en un lenguaje de programación para que un ordenador lo pueda ejecutar. En ese sentido, cualquiera puede programar. Y está genial que quien quiera hacerlo, por afición o por necesidad, aprenda. Es un poco como cocinar: cualquiera puede cocinar, pero no todo el mundo puede convertirse en chef.

Desarrollar software, por su parte, requiere considerar más cosas. Es una actividad compleja, que incluye elementos técnicos, sociales y lingüísticos. Desarrollamos software en equipos que integran personas, de negocio y técnicas. Software que debe servir a personas, nuestras clientas y clientes. Software que debe contribuir a lograr los objetivos de una empresa, que tiene que generar un valor, que tiene que responder a circunstancias cambiantes, que tiene que poder mantenerse en el tiempo, que tiene que tener fiabilidad y resiliencia.

Para conseguir esto no basta con saber programar. Es necesario considerar muchas más cosas. Por eso, este libro recoge algunas de ellas:

  • Principios que nos ayudan a tomar decisiones.
  • Patrones que nos ayudan a resolver problemas comunes.
  • Prácticas que resultan útiles para desarrollar software de forma sostenible.

Desarrollar software es algo más grande que saber programar. Es una profesión apasionante, que estamos construyendo entre todas y todos y en la que estamos constantemente aprendiendo, descubriendo formas nuevas y mejores de hacer cosas. Y también es una gran responsabilidad.

Nuestro trabajo tiene impacto en la vida de muchas personas. Puede facilitar su vida, potenciar sus capacidades, expandir sus relaciones personales, ahorrarles tiempo para dedicarse a lo que les importa de verdad, darles acceso a posibilidades nuevas, ayudarlas a disfrutar sus pasiones y un larguísimo etcétera.

Por eso tenemos que preocuparnos de desarrollar software que no solo funcione, sino que esté bien hecho, que se pueda entender, que sea honesto y fácil de intervenir en él.

Y me encantaría que este libro te ayude a hacerlo así.

I Principios

Los principios de diseño de software son guías, no mandamientos. Nos ayudan a tomar decisiones y tener conversaciones sobre si un software está bien escrito o no. Nos proporcionan criterios para juzgarlo, aunque están claramente abiertos a interpretación y ciertamente hay que tener en cuenta el contexto en que se aplican.

Ocasionalmente, pueden entrar en conflicto. Con frecuencia encontrarás que hay que buscar un equilibrio entre varios de estos principios. Sin embargo, es más habitual aún que colaboren unos con otros de tal modo que respetar uno de estos principios tiene como consecuencia que reforzamos el que se cumplan otros.

Por otro lado, es importante recordar que muchas veces estos principios no se refieren a líneas de código concretas, sino al conocimiento que estas representan. Intentar aplicarlos a la literalidad del código puede llevarnos a diseños extremadamente complejos que intentan solucionar problemas que no existen.

Es un poco como el Código de los Piratas: más bien son unas directrices...

Además, en esta parte veremos que hay vida más allá de los principios SOLID. En serio. Por supuesto, hablamos de ellos, pero también de otros que, o bien los completan, o bien son sus antecedentes.

SOLID es un acrónimo afortunado, muy fácil de recordar y hasta significativo: el software escrito tratando de respetar SOLID parece robusto, confiable y sólido. Y, de hecho, lo es.

Aunque debe su difusión a las publicaciones de Robert C. Martin, es posible que la primera persona que empezó a usar la expresión fuese Michael Feathers.

2 Cada cosita en su lugar

Edsger W. Dijkstra (1930-2002) es todo un personaje en el campo de las ciencias de la computación, no solo por la cantidad y calidad de sus aportaciones, sino también por su particular carácter y algunas frases lapidarias. También es responsable de introducir el principio de la separación de intereses, en su artículo de 1974 On the role of scientific thought.

On the role of scientific thought

Básicamente, el principio nos dice que los programas no deberían escribirse como una única pieza que resuelva el problema. En lugar de eso, debe organizarse en partes más pequeñas que se ocupan de tareas especializadas. O dicho de una forma más sencilla:

Diferentes partes del problema son tratadas por diferentes partes del programa.

Para ilustrarlo voy a usar un ejemplo exageradamente simplificado.

Consideremos este código en python:

 1 #!/usr/bin/env python3
 2 
 3 # Separation of concerns principle
 4 # Different parts of the program address different concerns
 5 
 6 import sys
 7 
 8 
 9 if __name__ == '__main__':
10     print(sum(map(int, sys.argv[1:])))

Este programa simplemente suma los números que se le pasan como argumento:

1 ./main.py 20 30 40   # --> 90

No parece tener nada incorrecto, ¿verdad? De hecho, este tipo de one liners suele considerarse como especialmente inteligente. Sin embargo, para este artículo, este código pone de manifiesto un problema.

En primer lugar, cualquier programa básico tiene tres partes, y es la primera separación de intereses que vamos a considerar aquí:

  • conseguir la información necesaria
  • procesarla para obtener un resultado
  • mostrar el resultado

En nuestro programa la única línea que tiene el programa se ocupa de los tres intereses. Esto quiere decir que si necesitamos modificar algo en relación con cualquiera de los tres intereses principales, tendremos que alterar todo el programa.

Da igual si se trata de mejorar algo en la presentación de resultados, obtener los números a sumar de otra fuente o utilizar un algoritmo diferente que haga el cálculo. Cambiar un aspecto del software implica hacer cambios que afectan a otros.

Podríamos simplemente separar cada uno de los intereses en una línea, algo así:

1 if __name__ == '__main__':
2     numbers_to_sum = map(int, sys.argv[1:])
3     sum_result = sum(numbers_to_sum)
4     print(sum_result)

Ahora hemos separado los tres intereses. Sin embargo, las líneas de código no representan bien las abstracciones que hemos definido. Para eso utilizamos unidades como las funciones:

 1 #!/usr/bin/env python3
 2 
 3 # Separation of concerns principle
 4 # Different parts of the program address different concerns
 5 
 6 import sys
 7 
 8 
 9 def obtain_input_data():
10     return sys.argv[1:]
11 
12 
13 def sum_numbers(input):
14     return sum(map(int, input))
15 
16 
17 def show_result(result):
18     print(result)
19 
20 
21 if __name__ == '__main__':
22     show_result(
23         sum_numbers(
24             obtain_input_data()
25         )
26     )

Ahora tenemos módulos diferentes que se ocupan de cosas distintas. El programa principal simplemente las coordina. Podríamos cambiar cualquiera de ellas internamente sin afectar al resto del código.

Puedes decir que no hay mucha diferencia. Sin embargo, ahora cada interés está siendo atendido por un módulo diferente del programa. Partes diferentes del programa se ocupan de intereses diferentes, en un mismo nivel de abstracción del proceso completo.

El principio de separación de intereses está en la base de otros muchos principios de diseño, entre ellos el Single Responsibility Principle y Tell, don’t ask. De esa relación nos ocuparemos más adelante.

Pero si únicamente pudieses quedarte con una idea de este libro, quédate con este principio.

3 Principio de abstracción

El principio de abstracción lo introduce Benjamin C. Pierce en el libro Types and Programming Languages, y viene a decir que:

Cada pieza significativa de funcionalidad en un programa debería estar implementada en un solo lugar del código. Y cuando hay funciones similares es beneficioso combinarlas en una sola, abstrayendo las partes que varían.

Seguramente este ha removido algo en tu interior y estés pensando:

— Pero eso, ¿no es el Don’t repeat yourself de Pragmatic Programmer?

Y no te falta razón. Ambos principios remiten a la misma idea de que es conveniente tener una única fuente de verdad para todo conocimiento que resida dentro del código. Así que vamos a enfocar cada capítulo en un aspecto diferente.

Hay dos puntos principales por los que tener en cuenta este principio.

Uno tiene que ver con el principio de separación de intereses de Dijsktra y la modularización. Al hacer que partes específicas del código se ocupen de asuntos diferentes, comenzaremos a ver que algunas de ellas podrían reutilizarse. Por ejemplo, si una función realiza determinado cálculo, no tenemos más que invocarla desde cualquier lugar del código en el que necesitemos ese cálculo. Esa función es la fuente de verdad sobre cómo actúa ese comportamiento específico.

El otro aspecto es que al evitar la duplicación, prevenimos comportamientos incongruentes entre diversas partes del código. Si tenemos un mismo conocimiento expresado en varios lugares distintos del código, es posible que en caso de realizar alguna modificación pasemos por alto alguna de sus versiones. Esto generará comportamientos del sistema diferentes según la forma en que se use, los cuales pueden ser difíciles de localizar incluso contando con tests.

Este principio nos llama a buscar la abstracción que pueda hacer única la fuente de verdad sobre piezas de funcionalidad de un programa y gestionar sus variantes parametrizándola. Como veremos al hablar de DRY es un proceso que entraña algunos riesgos, ya que es importante definir hasta qué punto la duplicidad de código representa realmente una duplicidad de comportamiento.

¿Y qué es una abstracción? Abstraer es un proceso por el que buscamos identificar los elementos esenciales de una idea, separándolos de las variaciones accidentales que observamos en sus ejemplos. Esa idea abstracta nos permite, entre otras cosas, comprender lo que nos rodea. Si ves un gato sabes que es un gato porque identificas ciertos elementos esenciales, pese a las muchas variantes en forma de tamaño, color, forma, longitud del pelo y un largo etcétera.

Abstraer, en programación, es básicamente identificar fragmentos de código que representan una misma idea aunque lo hagan de forma un poco diferente. A veces las detectamos al observar código duplicado casi literalmente. Sin embargo, en otros casos podría revelarse de una manera más sutil.

4 Command-query separation

Hablemos de CQS. No, de CQ-R-S no, sino de C-Q-S: el principio Command Query Separation. Enunciado por Bertrand Meyer, reza más o menos así:

Cada método debería ser o bien un comando que ejecuta una acción o bien una pregunta que devuelve información.

Esta distinción es fundamental y puede ayudar muchísimo a tu paz de espíritu y a la predictibilidad y testeabilidad de tu código. Es el no mezclar churras con merinas de la orientación a objetos. Por supuesto, es una aplicación del principio de separación de intereses. Y en realidad es muy fácil de respetar.

Empecemos por los comandos:

Un comando (command) produce un efecto o cambio en el estado del sistema. Por ejemplo: crea un nuevo registro en una base de datos, o genera un archivo en algún sitio. Lo que haga falta. Pero no devuelve ninguna respuesta: void. Si no ha fallado es que ha ido bien. Si algo ha ido mal lo mejor es que lance una excepción.

– Oye, y eso ¿cómo se testea?

– Pues se me ocurren dos formas:

  • Si es posible comprobamos que se ha producido el cambio deseado en el sistema.
  • En el nivel unitario, usamos mock para verificar que se ha enviado el mensaje que produce el efecto.

– ¿Y si devuelve OK o true o ACK para indicar que ha ido todo bien?

– ¿Qué tiene de malo tirar una excepción? De otro modo tienes que esperar una respuesta y comprobarlo. Eso te lo da try/catch y es bastante más bonito.

Y no te confundes con las queries.

Las preguntas, o queries, por su parte devuelven una respuesta informando sobre el estado del sistema: ¿me das el producto con Id 1234? ¿Cuánto suman 2 y 3,45? ¿Cuál es el sentido del universo y todo lo demás?

El problema que tienen las queries es que, ya que estamos podríamos hacer algo, además de preguntar. Pues oye: NO. Porque ese hacer algo es producir un cambio en el sistema y si al hacer una pregunta cambias el estado: ¿cómo te puedes fiar de la respuesta?

Imagina un sistema de coche autónomo que al preguntar las rpm del motor también las cambia. Por ejemplo, las aumenta un 5%. ¿Qué te indica la respuesta de las rpm? ¿Las mide antes o después de del cambio? ¿Qué consecuencias tiene eso? Básicamente que no puedes confiar en la respuesta.

Por eso deben separarse las acciones que cambian el sistema (command) de las acciones que obtienen información sobre el sistema (query). Por cierto, testear las queries es bastante sencillo, ya que solo tenemos que verificar la respuesta que devuelven.

Si una query produce algún efecto en el sistema hablamos de side effects y es algo que debemos evitar. Por el buen funcionamiento del sistema y porque de este modo podemos testear con confianza.

Una clase puede tener tanto commands como queries siempre que respetemos el SRP, claro. El principio CQS se refiere únicamente a que un comando no devuelva respuestas: se ejecuta y confiamos en que irá bien; y a que una query no produzca side effects poniendo el sistema en estado indeterminado. Y nadie quiere eso.

Llevando eso a objetos-método, como pueden ser los Casos de Uso, tenemos que separar aquellos que son comandos de los que son queries. Y aplica lo mismo y por las mismas razones.

¿Y CQRS? CQRS son las siglas de Command Query Responsibility Segregation y es algo así como llevar CQS hasta las últimas consecuencias, separando los commands, que implican escrituras, y las queries, que implican lecturas, incluso al nivel de la infraestructura. Por ejemplo, tendrías una base de datos para escritura, pero leerías los datos de una o más réplicas. Pero CQRS es más un patrón que un principio.

En cualquier caso, la separación command-query es fundamental para construir sistemas confiables y fáciles de mantener. No hay nada peor que un side effect ocurriendo vaya usted a saber dónde.

5 No te repitas

Ya que hoy no llueve, hablemos del principio DRY.

Personalmente, creo que en programación tenemos un problema con los acrónimos ingeniosos. El caso es que DRY son las siglas de Don’t Repeat Yourself (no te repitas). Es como el Principio de Abstracción de Pierce, pero expresado de una manera menos formal.

Este principio, enunciado por Hunt y Thomas en The Pragmatic Programmer, dice que:

Toda pieza de conocimiento debe tener una representación única, no ambigua y autoritativa dentro de un sistema.

Toda pieza de conocimiento, ¿lo pillas? No de código. El principio DRY no habla de duplicación de código. Pero encontrarás cientos de artículos en Internet que dicen algo como aplicar DRY para eliminar la duplicación de código. Seguro que yo mismo he incurrido en eso alguna vez.

Y, a su lado, otro trillón de artículos alertando de lo pernicioso que es el principio DRY que te puede llevar a la abstracción incorrecta. O del código WET: Write Everything Twice.

Pero no. DRY aplica ni más ni menos que a la duplicación de conocimiento, lo que se refiere tanto a código como a datos. Así, por ejemplo, mantener dos fuentes de verdad y tener que sincronizarlas es un problema derivado de no haber pensado en DRY.

En cuanto a código, DRY puede ocurrir tanto con fragmentos de código similares, la consabida y duplicación de código, como fragmentos diferentes. Quiero decir que el problema no es la repetición literal de código per se, sino la duplicación de conocimiento.

Como tantas otras cosas en código, suele ser más fácil de diagnosticar cuando tienes que introducir cambios: si dos fragmentos de código tienen que cambiar a la vez es muy posible que tengamos una violación del principio DRY.

¡Ah! Pero también del principio de cohesión, pues dos cosas que habitualmente cambian juntas deberían estar juntas. Los que nos dice DRY, a mayores, es que incluso podrían ser la misma, representando el mismo conocimiento.

Obviamente, una duplicación de código puede llevarnos a pensar que estamos ante un problema relacionado con DRY. Pero hay que andarse con ojo aquí.

Como heurística, puedes aplicar la regla de los 3: espera a tener al menos 3 ejemplos de duplicación antes de pensar que ahí se esconde una abstracción. Y aún haciéndolo así tienes tres posibilidades:

  1. que esa duplicación sea inevitable: fragmentos de código muy similares que se ocupan de cosas diferentes.
  2. que no haya una abstracción ahí, pero que sí sea posible reducir la duplicación estructural en ese contexto, posiblemente extrayendo un método.
  3. que las duplicaciones sean casos de una abstracción.

Respecto a la segunda podría argumentarse que esa duplicación representa un conocimiento en ese contexto reducido, pero no generalizable a todo el sistema, no-sé-si-me-explico. Aquí podemos aplicar la regla del tres, que nos dice que no intentemos resolver la duplicación hasta no tener al menos tres casos.

Por esa razón, se recomienda evitar las abstracciones prematuras: ¿cambian esos códigos repetidos a la vez o lo hacen por separado? Si no cambian a la vez, posiblemente están representando conocimientos diferentes.

DRY es un principio necesario para garantizar que un sistema tiene fuentes únicas de verdad y contribuir de este modo a su consistencia. Pero DRY no es un principio orientado a eliminar la duplicación de código. Esta es solo un indicio, o smell, de una posible duplicación de conocimiento.

En resumen, DRY:

  • Una sola fuente de verdad para cada conocimiento en el sistema
  • Si cosas diferentes cambian juntas: aplica cohesión y DRY
  • Duplicación de código no implica necesariamente una abstracción

6 Falla pronto

Fail Fast, o falla pronto, como principio de diseño de software puede parecer contra-intuitivo. Es un principio que he visto atribuido a Jim Gray y que viene a decir:

Un módulo que falla rápido detecta y comunica errores y deja que el módulo inmediato superior los gestione.

Fallar rápido quiere decir que en cuanto se detecta algo que no está bien no tratamos de arreglarlo en el punto de detección, ni fallar silenciosamente. Al contrario, tiramos una excepción o error que suba por la pila de llamadas hasta encontrar quién sabe gestionarlo.

Imagina: un método requiere un parámetro entero en el rango de 5 a 27. Si el valor es menor o mayor… excepción. Quien haya llamado a ese método con el valor incorrecto, será quien tenga que gestionar el error. Nada de intentar arreglarlo u ocultarlo bajo la alfombra.

Esto tiene sentido. El objeto que haya hecho la llamada debería tener más contexto para resolver el problema y decidir si puede usar otro valor. Por ejemplo, si ha pasado uno mayor podría emplear el límite superior en su lugar, o si tiene que fallar igualmente y hacer subir el error un nivel más.

El objeto en el que se detecta el error carece del contexto necesario para tomar decisiones. Por tanto, es mejor fallar y hacerlo sonoramente.

A veces, el lenguaje lo hace por nosotras. Con el tipado estricto de parámetros, el compilador o intérprete fallará si llega un valor de tipo incorrecto. El tipado de retorno hace lo mismo.

Por supuesto, hay condiciones que son del negocio, como el ejemplo de que exista un rango de valores admisible. Estas reglas pueden considerarse precondiciones, y cuanto antes las evaluemos, mejor. Para esto, podemos usar cláusulas de guarda o asserts.

Que los asserts no son solo para los tests. Se trata de pequeños objetos que encapsulan una regla y lanzan una excepción si les pasamos un objeto que no la cumpla. Los asserts nos permiten verificar que un valor cumple ciertas condiciones, fallando si no es así. Si no tienes Asserts, puedes usar cláusulas de guarda: if algo_esta_mal then lanza excepción.

Contra lo que pueda parecer, un sistema que falla rápido es muy robusto. En caso de errores, permite localizarlos más fácilmente y garantiza que si un valor pasa todos esos controles podemos confiar en él siempre.

Una aplicación de este principio es la construcción de objetos con validaciones que no permiten instanciar un objeto si no se cumplen las reglas de negocio. De este modo, si un objeto ha podido ser creado, es que es válido y consistente. No necesitas chequearlo cada vez.

Si tienes un módulo que sientes que debería gestionar el error detectado, es posible que tengas un problema con el diseño y debas separar cosas ahí. Típico caso: bucle que lee datos de un archivo y que valida cada línea para poder incluirla o no. En ese caso conviene aplicar la separación entre el bucle y el proceso del ítem, de modo que el bucle gestione los posibles errores de procesar el ítem.

Así que, ya sabes, en lo tocante a hacer sistemas robustos: falla pronto y falla sonoramente.

7 Hazlo estúpidamente simple

KISS no es la banda de glam rock, ni un beso. Es un principio formulado por Kelly Johnson, ingeniero de la Lockheed. Allí participó en el diseño de una buena lista de aviones militares, muchos de los cuales son hitos de esa industria. Su enunciado:

Keep it simple stupid (sin coma).

Efectivamente, la forma original no lleva coma y pierde ese matiz borde de “que sea simple, estúpido”. La estupidez se refería al diseño y no al diseñador. O puede que fuese:

Keep it simply stupid.

¿En qué sentido debería ser estúpido un diseño?

Primero, un poco de contexto sobre el principio. Un avión militar no es una cosa sencilla, sino bastante compleja. El propósito de Johnson era diseñar aviones cuyos elementos fuesen fáciles de reparar en condiciones adversas, como no disponer de personal cualificado, situaciones de combate, escasez de repuestos, etc.

Así que la idea era mantener los componentes simples y fáciles de reparar, sin grandes sofisticaciones que requiriesen personal o herramientas muy especializadas.

Aplicado al desarrollo de software, podríamos interpretarlo como desarrollar componentes que sean estúpidos y simples: poco inteligentes, fáciles de comprender en un vistazo, en los que sea muy sencillo entender lo que ocurre y cómo funcionan.

Una forma de lograr esto, a posteriori, es descomponiendo partes de código muy enrevesadas y complejas en funciones o métodos pequeños con nombres descriptivos, de forma que cada una de ellas sea fácil de entender aisladamente.

Otra manera de lograrlo es escogiendo soluciones sencillas en primer lugar. Por ejemplo: un test KISS solo probaría una cosa, con un único ejemplo. Una función KISS usaría código simple, tomando pocas decisiones. Para tomar decisiones complejas combinaríamos varias funciones KISS.

Es difícil explicar esto último sin un ejemplo, pero toda la idea de programar sin if de la orientación a objetos es muy KISS. Parece complicada, pero en realidad apuesta por tener pequeños elementos que son muy fáciles de entender aisladamente. La clave es tomar primero la decisión de qué objeto simple deberá realizar un trabajo, en lugar de tener objetos complejos que toman decisiones.

La composición de objetos también es muy KISS, porque permite construir comportamientos complejos a base de combinar objetos simples.

Es decir: KISS va de tener elementos que tomados por separados resultan estúpidamente simples, pero combinados desarrollan comportamientos que pueden ser tremendamente complejos.

Al tener componentes muy simples es fácil cambiarlos por otros, es fácil detectar y corregir errores, es fácil entenderlos y razonar sobre ellos, podemos tener todo su código en la cabeza, por así decir, y es muy fácil probarlos unitariamente.

La sencillez es la sofisticación definitiva. Pero hacer cosas sencillas conlleva mucho trabajo.

8 El mínimo conocimiento

No hables con extraños. Seguro que más de una vez te dijeron esto las primeras veces que salías de casa sin compañía adulta, aunque solo bajases a la tienda del portal de al lado a buscar el pan. También es una de las formulaciones de un principio de diseño de software.

Que no es que aclare mucho, aunque sí algo más que su nombre más popular: La ley de Demeter, que formalmente se conoce como Principio del mínimo conocimiento.

Todo el clickbait de esta introducción es para hablar de uno de los principios que ayuda a reducir el acoplamiento en programación orientada a objetos. Fue introducido por Ian Holland como regla en un proyecto llamado Demeter, de donde recibe su nombre.

El principio de mínimo conocimiento viene a decir que:

Los objetos deben tener el mínimo conocimiento posible sobre otros objetos.

Exactamente, el que exponen a través de sus API públicas. Para entendernos, digamos que por conocimiento aquí entenderemos a la capacidad de interactuar y usar al otro objeto. El problema viene si hacemos trampa.

Imagina que tienes un objeto Service y otro Consumer. Consumer usa Service a través de su interfaz pública que expone un método doServiceThing. Hasta aquí todo bien.

Resulta que Consumer necesita escribir en un log. Consumer no tiene logger, pero Service sí. Como tú sabes eso, haces que Service pueda entregar su Logger y lo usas. Todo bien, ¿no? Pues no.

Para empezar Consumer no tendría que saber que Service tiene un Logger, ni tú tendrías que exponerlo para que otros objetos puedan usarlo, ya que no es la responsabilidad de Service. Service hace doServiceThing y eso es todo lo que debería preocuparle a Consumer.

Al usar Logger, Consumer está hablando con un extraño. Sabe algo de Service que no debería saber, por lo que su acoplamiento con Service aumenta. Me explico:

Si Consumer solo usa la interfaz pública de Service, podríamos abstraer esta interfaz (doServiceThing) de modo que otros ServicesDoingThings pudiesen implementarla y, por tanto, Consumer podría usar otro más conveniente.

Pero puesto que Consumer utiliza algo de Service que solo tiene Service y que Consumer sabe que tiene, resulta que Consumer está fuertemente acoplado con Service y no puede usar otra implementación.

Este drama es una de las cosas que intenta evitar el Principio de Mínimo Conocimiento. Que, en este ejemplo, estaría estrechamente relacionado con el Principio de Sustitución de Liskov, del que ya hablamos en otro capítulo.

En la práctica, el Principio se utiliza así: en un método de un objeto solo se puede hablar con objetos conocidos y no puede hablar con extraños. ¿Qué objetos puede conocer un método? A saber:

  • El objeto que contiene el método. √
  • Objetos que recibe como parámetros en ese método. √
  • Objetos que se instancian en el método. √
  • Objetos que son miembros del objeto que tiene el método. √.

Pero, ¿quiénes son desconocidos?

Todos los demás, incluyendo objetos que puedan ser entregados por otros objetos. Esto último suena rarísimo, porque básicamente obliga a que:

  1. Consumer pase el objeto recibido como parámetro a otro objeto o método y actúa como intermediario. Esto puede llevar a cadenas de indirección largas como días sin pan.
  2. Si Consumer solo necesita una respuesta o acción del objeto recibido, Service tendría que entregar esa respuesta o ejecutar la acción sin exponer su estructura interna. Esto puede generar interfaces con muchos métodos.

Por tanto, hay que combinar el principio con otros. Ya hemos mencionado el de sustitución, pero también tendríamos que fijarnos en el de Segregación de Interfaces.

¡Compromisos! ¡Compromisos!

Sin embargo, este principio ayuda mucho a reducir el acoplamiento. Una regla muy práctica para ver si tenemos problemas relacionados con él es la de un solo punto (en PHP sería una sola flecha), referida a la manera de invocar métodos en objetos con la notación object.method.

Si hay más de un punto,(object.method1.method2) es que posiblemente estamos rompiendo el principio de mínimo conocimiento. No confundir esto con las interfaces fluidas, que devuelven la misma instancia del objeto.

Así que, en resumen, el principio de mínimo conocimiento nos dice que los objetos solo deberían interactuar con otros objetos de los que tengan conocimiento directo a través de su interfaz pública y no basarse en lo que sepan de su estructura interna, que debería ser exactamente nada.

En Programación Orientada a Objetos, los objetos son cajas negras que exponen una manera de interactuar con ellos y no tenemos que saber nada de su estructura interna, y mucho menos usarla fuera del objeto.

9 Dime, no preguntes

Más que un principio este es un consejo de los pragmáticos Hunt y Thomas, que es muy útil para ayudarnos a mantener los principios de encapsulación y ocultamiento de información. Pura orientación a objetos.

Incluso diría que es más bien un consejo de refactor. Viene a decir que:

No deberías tomar decisiones basándote en el estado de un objeto si van a resultar en que cambies el estado del propio objeto.

Es una forma un poco liosa de expresarlo. La cuestión es que en OOP el estado (propiedades) de un objeto deberían estar oculto (ocultación de información) y cambiaría como resultado de enviar mensajes a ese objeto (invocando sus métodos). El objeto es responsable de gestionar su propio estado (encapsulación).

Salvando el caso de los que solo son portadores de datos, no deberíamos tener acceso directo a las propiedades de los objetos. Las propiedades son privadas, de hecho en algunos lenguajes lo son por defecto, ni exponemos getters ni setters, para no exponer la estructura.

Dicho de otro modo, lo que contengan los objetos no nos importa para nada. Solo debemos preocuparnos de lo que pueden hacer.

Y ahora mismo me pregunto si los DTO deberían ser considerados objetos o más bien tipos (structs) inmutables. Ahí lo dejo.

Entonces, ¿cómo se cambia el estado de los objetos? Mediante el envío de mensajes, o invocación de métodos para entendernos, que produzcan el cambio de propiedades y que tengan valor semántico en el dominio de esa pieza de software.

Los mensajes que un objeto entiende conforman su interfaz pública. Una interfaz pública bien definida o contrato, aunque no hace falta que sea definida explícitamente, hace que tengamos total libertad para cambiar la implementación del objeto. Siempre que mantengamos esa interfaz.

Este es uno de los cambios de mentalidad más complicados cuando venimos del estilo procedural. En orientación a objetos, estos nos ocultan información, a cambio de ofrecernos comportamiento y colaboración.

Un problema es que solemos intentar explicar orientación a objetos a partir de sus propiedades, es decir, de cómo convertimos la expresión de ciertos conceptos mediante tipos primitivos en objetos. Y tendemos a verlos como contenedores de datos.

Sin embargo, el enfoque sería tratarlos como information experts, es decir: cada objeto sabe de los suyo y es a quién tenemos que pedirle cosas. Este entronca con los llamados patrones GRASP, de los que ya hablaremos. Pero puedes empezar pensando en que un objeto debería ser el máximo experto del sistema en lo suyo y, por tanto, reunir todos los comportamientos que le correspondan semánticamente hablando. Esto contribuye al SRP y DRY.

Otro aspecto es que los objetos deberían crearse consistentes, con todo lo que necesiten para funcionar y cumpliendo las reglas de negocio, de modo que no tengas que preguntar al objeto si está completo o bien formado: si está en el sistema es que puede trabajar.

Por tanto, si tienes que preguntarle al objeto cómo estás para decirle cómo deberías estar, significa que esa operación concreta tendría que formar parte de las habilidades de ese objeto.

10 Única responsabilidad

Single Responsibility Principle (SRP) es uno de los principios de diseño orientado a objetos que más discusiones provoca. Todo gira en torno a la idea de Responsibility: ¿qué significa? El principio dice que:

Las unidades de software solo deberían tener una responsabilidad.

A veces esto se interpreta como las clases solo deberían hacer una cosa. Pero este camino es una vía muerta porque:

  • no define cuál es el alcance de esa cosa.
  • puede llevar a diseños excesivamente complicados.

El SRP está modulado por otros principios. En realidad todos los principios de diseño OO se modulan mutuamente.

El SRP extiende del más general separation of concerns: partes distintas del software se ocupan de asuntos distintos. Pero la magnitud de la separación depende del principio de cohesión: lo que cambia junto permanece junto y se separa lo que no cambia.

Una de las claves del SRP está en mantener ese equilibrio, pero la otra clave es cómo se define la responsabilidad de una clase.

La propuesta de Robert C. Martin es que responsabilidad es una única razón para cambiar. Por tanto: las clases solo deberían tener una única razón para cambiar. Obviamente, esto no zanja el problema: ¿Qué es tener una única razón para cambiar?

En realidad, habría que pensar más en agentes de cambio. Un ejemplo sería una clase que pueda esperar cambios por parte de varios equipos en una empresa, como podrían ser Finanzas y Clientes, o Adquisición y Ventas.

Los cambios originados en uno de los equipos podrían ser indeseables para el otro. Cuando estamos en esta situación lo apropiado sería repartir las responsabilidades o razones de cambio en diferentes clases, incluso aunque representen el mismo concepto.

Pero esto, a su vez, tiene múltiples formas de manifestarse. El ejemplo anterior, desde una perspectiva de DDD, nos estaría diciendo que un mismo concepto puede ser visto de manera diferente según el contexto: Bounded Contexts.

Claro que lo anterior puede ocurrir en el caso del diseño de la capa de dominio en una aplicación de negocio. Es un nivel muy alto de abstracción. ¿Aplica el SRP en niveles más bajos? Yo diría que sí.

Por ejemplo, es buena idea separar la obtención del contenido de un archivo y su procesamiento. De hecho el primer paso es muy genérico, mientras que el segundo es específico de nuestra aplicación y cambia por razones diferentes: unas técnicas y otras de dominio.

A su vez, la obtención del archivo puede tener varias razones para cambiar. Por ejemplo, será distinto si usa el sistema de archivos local, S3, FTP… Con SRP en lugar de una clase que lea cualquier tipo de sistema, tendríamos clases diferentes, seleccionables por su consumidor.

Esto es: separo obtención y procesamiento, pero también separo distintas estrategias de obtención. La cuestión es que el principio actúa a la vez sobre distintos niveles de abstracción. Por ejemplo:

El servicio que se ocupa de obtener el resultado final tiene la responsabilidad de procesar un archivo, y para ello usa como colaboradores un módulo que obtiene archivos y otro que los analiza.

El módulo de obtención tiene la responsabilidad de obtener una representación procesable del archivo físico, para lo que usa distintos adaptadores y posiblemente una factoría para escoger el adecuado.

A su vez, cada adaptador es responsable de entenderse con el sistema físico que almacena los archivos. La factoría sabe como montar cada adaptador.

El módulo de procesamiento tiene la responsabilidad de convertir la representación recibida en lo que sea que es importante para el dominio de la aplicación.

Parece como que el servicio tiene muchas razones para cambiar al englobar todos los componentes. Sin embargo, el servicio ignora los detalles de esos componentes, su única responsabilidad es coordinarlos y cambiará si esa coordinación cambia.

La responsabilidad del módulo de obtención es cargar el contenido del archivo y delega el entenderse con el sistema físico al adaptador que toque. Su responsabilidad es entregar el contenido, para lo cual coordina varias cosas: elegir el adaptador y entregar su salida.

La responsabilidad de un adaptador sería entregar el contenido del archivo indicado en la forma que se le pide. Hace varias cosas, como comunicarse con el sistema físico y transformar los datos para que el módulo superior los pueda manejar.

En resumen: cada nivel de abstracción nos proporciona un contexto en el cual interpretar y aplicar el SRP.

Otra forma de verlo es que al aplicar el SRP podemos usar varias estrategias. A veces tendremos que separar en un mismo nivel de abstracción y otras veces tendremos que separar y delegar cada parte.

El SRP, bien aplicado, ayuda a que el software sea más mantenible al delimitar las fuentes de cambio de una clase. Pero hacer que las clases sean SRP puede hacer que los diseños ganen cierta complejidad.

Obviamente, es un compromiso, pero diseñar es básicamente decidir qué compromisos estamos dispuestas a aceptar.

11 Abierto para extensión, cerrado para modificación

Open-Closed es la O de SOLID. Se refiere a situaciones en las que un cambio de funcionalidad podría suponer un cambio en un código en producción, como por ejemplo, en una clase, por simplificar. Obviamente no aplica cuando estamos desarrollando o diseñando. Por tanto, insisto en que nos estamos refiriendo a código en producción. Dice así:

Las clases deberían estar abiertas para extensión y cerradas para modificación.

Tenemos un problema si para modificar la funcionalidad:

  • Tienes que volver compilar todos los módulos que usan esa clase. Ahora igual no es tan importante, hace unos años podía ser un marrón.
  • El cambio de funcionalidad no se aplica a todos los usos de esa clase, lo cual es problemático, porque podría introducir efectos indeseados en lugares que no esperamos.

En lenguajes interpretados tampoco supone mucho problema pues no tienen tiempo de compilación, pero la posibilidad de introducir errores o cambio indeseado de comportamiento afecta igualmente.

Para prevenir eso se aplica el principio de que las clases sean abiertas a extensión y cerradas a modificación. De este modo, aprovechas la clase extendiéndola sin perjudicar los usos de la clase que no están afectados por el cambio.

Como no se puede predecir el futuro y lo más posible es que YAGNI (no lo necesitarás), tampoco habría que agobiarse en diseñar cada clase para que sea open-close certified.

Si, llegado el caso, la clase no está abierta a extensión, lo adecuado es hacer un refactor preparatorio que la abra, manteniendo su comportamiento actual, pero permitiendo alguna forma de extensión.

By the way, esto es más fácil si las clases tienen responsabilidades bien definidas y no tienen múltiples razones de cambio.

Pero, por otro lado, tampoco tiene sentido hacer open-closed cualquier clase. Una asunción del principio es que te interesa conservar la funcionalidad de la clase actual. Otra es que duplicarla, creando una clase nueva para la nueva feature, por ejemplo, es demasiado caro por compilación, mantenimiento, etc..

Y es que, en muchos casos, añadir una clase nueva duplicando una existente es una buena solución para el mismo problema. Pero hay que valorar el coste de duplicación frente a los beneficios.

Por otro lado, ¿de qué cambio estamos hablando? En algunos casos puede ser parametrización de un comportamiento, en otros casos de un cambio en el algoritmo, en otros modificar sobre el output.

Para ello tenemos diversos patrones aplicables, como puede ser Strategy, en el que el consumidor puede decidir el algoritmo, o Decorator, que transforma el output según el consumidor.

En general, si una clase tiene responsabilidad única es fácil hacerla extensible. Si no, será complicado.

Pero tampoco hay que agobiarse intentando hacer que todo sea open-closed por si acaso. Por encima de cualquier decisión suele estar YAGNI, no adivines el futuro, y siempre podemos hacer refactor para abrir las clases cuando toque.

12 Sustitución de Liskov

El principio de sustitución de Liskov es la L de SOLID y se refiere a cómo se definen las jerarquías de herencia. Fue enunciado partir de un artículo de Barbara Liskov y Jeannette Wing, titulado: A Behavioral Notion of Subtyping.

A Behavioral Notion of Subtyping

En resumen, lo que dice es que el subtipado, o sea, la creación de tipos de datos derivados de otros, no es una cuestión puramente sintáctica, sino también semántica. En otras palabras: los tipos y sus subtipos derivados representan comportamientos equivalentes.

Los subtipos deberían ser sustitutos de sus clases base

Una forma de ver esto es que todas las clases de una jerarquía deberían ser intercambiables. Veamos un ejemplo: supongamos una clase User que tiene dos clases hijas: Customer y Administrator. Estas subclases son especializaciones de la clase base.

Pues bien, desde el punto de vista de sus consumidores esas clases deberían ser intercambiables. En otras palabras: tendrías que poder usar tanto Customer como Administrator, sin tener que cambiar el código consumidor.

Si puedes intercambiar Customer con User y Administrator con User, entonces podrías intercambiar Customer con Administrador. En conclusión: las clases de una jerarquía deberían ser intercambiables. Esto no quiere decir que se tengan que intercambiar realmente, sino que desde el punto de vista del consumidor, da exactamente igual que le pasemos cualquiera de ellas. No depende del tipo concreto que se haya pasado, porque las clases de la jerarquía tienen que tener un comportamiento equivalente desde el punto de vista de su consumidor.

Nuestro ejemplo de User, Customer y Administrator, puede ser válido porque su comportamiento en un sistema sería equivalente. Eso no quiere decir que sea el mismo, pues tienen distintas capacidades de acción en el programa.

Ahora otro ejemplo: tenemos una clase Logger que escribe logs. También queremos hacer una clase Service que necesita escribir logs, así que hacemos que Service extienda de Logger.

¡Wrong! ¡Todo mal!

Estructuralmente hablando, podrías hacer eso, pero semánticamente no.

La responsabilidad de Service no sería escribir logs aunque necesite poder hacerlo. Semánticamente, nuestro Service no es un Logger. Si cambiamos Service por Logger no tendremos el comportamiento que el consumidor espera.

Dicho de otra forma: No debes extender una clase para reutilizar funcionalidad a partir de otra no relacionada. En su lugar, utiliza la composición. No hay nada que impida a Service usar a Logger, pero Service no puede ser hija de Logger.

La herencia no es un mecanismo para compartir comportamiento, sino para especializar comportamiento. La clase base contiene el comportamiento común y las derivadas las versiones especializadas.

Por ejemplo, una empresa proveedora de energía podría tener Contract como clase base y ElectricityContract y GasContract como clases derivadas. Ambas son Contract, pero especializadas en un tipo de suministro.

Con todo, el principio tiene discusión. ¿Qué pasa si las clases base son abstractas? ¿Aplica esto a familias de clases que implementan una interfaz?

En su formulación original el principio no contempla esto, pero creo que se puede aplicar si lo reformulas de esta manera: los subtipos no deben romper los contratos establecidos por los tipos de los que descienden.

Una recomendación relacionada es que las jerarquías de clases tampoco deberían ser muy profundas: mejor un nivel que dos. Por otro lado, si sientes que querrías tener herencia múltiple es posible que necesites composición en vez de herencia.

Recuerda que la herencia es el mayor acoplamiento posible entre clases.

Una limitación que se puede extraer del principio de sustitución es que una clase derivada no puede establecer nuevos contratos. Es decir: no puedes tener métodos en una subclase que no estén en su clase base.

De ser así, el consumidor quedaría acoplado a la subclase porque no habría forma de reemplazar esta con su ancestro u otras de la familia.

Claro que esto nos podría llevar a situaciones en las que una clase arrastra métodos que están en el contrato de su ancestro y que no necesita realmente. Y ahí entra el principio de Segregación de Interfaces del que hablamos en otro capítulo.

En pocas palabras: los contratos, o interfaces, también deberían ser pequeños y muy centrados en las necesidades de los consumidores.

Así que, según el principio de sustitución las clases que derivan de otras tienen que ser intercambiables. Eso hace posible aprovechar el polimorfismo de modo que un consumidor pueda usar distintas versiones o especializaciones de un concepto sin tener que saber de cuál de ellas se trata.

13 Segregación de interfaces

El Principio de Segregación de interfaces es la I de SOLID. No es del todo fácil explicar este principio sin poner ejemplos de código, pero vamos a intentarlo.

En pocas palabras, el principio pide diseñar interfaces con pocos métodos de modo que sus clientes no necesiten depender de los que no van a usar. Lo mismo las clases que las implementen o extiendan.

Nos exponer métodos a un consumidor que este consumidor no use

Veamos un caso típico: Tienes una clase que implementa una interfaz y ves que necesita un método nuevo para un cierto caso particular. Lo añades, pero tendrás que añadirlo a la interfaz, o a la clase base, y a todas sus hermanas si existen. Y posiblemente será vacío porque ese comportamiento no se necesita en ningún lugar más. Ahí tienes una violación de segregación de interfaces.

Y de Liskov de paso. El nuevo método está pidiendo una interfaz diferente que implementará esa clase específica, así no tienes que contaminar al resto de la familia. La cuestión es: ¿por qué el nuevo método?

Pues porque algún cliente de la clase lo requiere, lo que nos lleva a una de las definiciones del principio: interfaces finamente granulares específicas por cliente. Por supuesto, apunta a que las interfaces se deben diseñar a partir de lo que requieren sus usuarias.

Una clase que acumula muchos métodos probablemente está violando SRP y lo mejor sería extraer varias interfaces basándonos en sus consumidores, de modo que cada uno dependa solo de los métodos en que está interesado. Al principio, podemos hacer que la clase acumuladora las implemente todas.

Posteriormente, haríamos un refactor para repartir esa funcionalidad en clases más pequeñas que implementen cada cual su interfaz correspondiente, ganando en capacidad de mantenimiento. Puedes aplicar el patrón adapter usando la clase acumuladora como colaboradora en una primera aproximación.

Si necesitamos introducir un método nuevo en una clase, plantéate si no sería mejor introducir una nueva interfaz antes de nada. Especialmente si esa clase ya implementa una interfaz o es una subclase.

Una clase que tiene que implementar o sobreescribir un método para que no haga nada porque está obligada a tenerlo por herencia o por interfaz, nos está indicando una violación del principio. Y puede que falle single responsibility en la interfaz o la clase base.

Este principio está estrechamente vinculado con SRP y Liskov, incluso con OCP. Quizá sea uno de los mejores chivatos de mal diseño orientado a objetos.

Así que si ves métodos sin implementación porque tienen que cumplir la interfaz, o clases con dependencias de las que solo usan un pequeño porcentaje de métodos, tienes entre manos un caso de violación de Segregación de Interfaces.

14 Inversión de dependencias

Ya tengo ganas de terminar con SOLID, así que hablaremos del principio de Inversión de Dependencias. Contribuye a que el código sea sostenible y flexible. Y no hay que confundirlo con el patrón Inyección de Dependencias.

Dice así:

Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Ambos deberían depender de abstracciones
Las abstracciones no deberían depender de los detalles. Los detalles deben depender de abstracciones.

Alto nivel, abstracciones, detalles… Otra forma de enunciarlo es que las dependencias deberían apuntar en el sentido de mayor estabilidad. Las abstracciones serían más estables que las implementaciones.

¿Qué es un detalle? Detalle es una implementación concreta. Por ejemplo: un detalle sería una tecnología de persistencia, como una base de datos relacional. ¿Y una abstracción? Pues por ejemplo, un repositorio. El repositorio se puede implementar usando diferentes tecnologías de persistencia, pero la idea de repositorio es abstracta y se representa mediante una interfaz (explícita o no) que expone comportamientos como guardar, recuperar y seleccionar. La interfaz es lo que nuestra aplicación “ve”, podemos cambiar la implementación sin afectarla siempre que cumplamos el principio de inversión de dependencias, de tal modo que:

  • La interfaz (abstracción) del repositorio no dependa de una implementación concreta (detalle)
  • Cada implementación concreta depende de la interfaz.

En la práctica, esto significa que la aplicación no sabe si está usando una base de datos relacional, noSQL, o un JSON o una API, o un doble de test. Hay gente que dice que “es que (casi) nunca vas a cambiar la base de datos, no importa”… Pues no: si haces tests, cambias de base de datos porque los dobles (stubs, mocks o fakes), son implementaciones alternativas de la abstracción “repositorio”. Y quien dice repositorio, dice cualquier otro concepto que pueda tener múltiples implementaciones.

El principio también actúa como una propiedad de toda arquitectura multicapa sostenible. Las dependencias apuntan hacia las capas más abstractas. En la clásica “Dominio/Aplicación/Infraestructura”, Infraestructura depende de Dominio, Aplicación usa los elementos de Dominio y Dominio no tiene dependencias, porque es la “abstracción” de negocio. Dominio define las abstracciones que luego se concretan en las otras capas (principio de segregación interfaces). Y dominio no tiene ninguna dependencia. De este modo, se pueden posponer decisiones de implementación, mientras desarrollamos el core de la lógica de negocio. Podríamos trabajar con componentes fake (como repositorios en memoria, stubs de ciertos servicios y otros) hasta decidir qué tecnología nos conviene más. O cambiarla si necesitamos escalar en algún momento, simplemente escribiendo un nuevo adaptador.

La violación de este principio suele tener consecuencias bastante problemáticas. Caso típico de los frameworks “database-first” en lo que la aplicación acaba estando acoplada a un detalle. Cuando necesitas crecer descubres que todo el código está acoplado a la base de datos vía Active Record en los lugares más insospechados. Con otros ORM también es fácil que ocurra esto.

A la hora de asegurarnos que nuestro código cumpla con Inversión de Dependencias, es importante definir interfaces estables (explícitas o no) y favorecer la composición frente a la herencia también ayuda.

15 No lo vas a necesitar

Your ain’t gonna need it es una expresión acuñada por Ron Jeffries para indicar que no deberías estar desarrollando prestaciones en el software si nadie te las ha pedido o por si en el futuro resultasen ser valiosas.

No lo vas a necesitar

La dificultad con YAGNI es que muchas personas se vienen arriba creando código por si acaso. A veces resulta tan evidente la necesidad de introducir alguna característica, aunque nadie la vaya a usar a corto plazo, que resulta irresistible añadirla. Incluso sin tests, ya puestos.

Esto puede generar varios problemas. Al introducir código que no se está usando ni se usará, añadimos ruido a nuestro software. Cuando alguien pase por ahí tendrá que preguntarse: ¿Qué es esto? ¿Afecta a lo que estoy haciendo? ¿Podría verse afectado por lo que estoy haciendo? ¿Está cubierto por algún test? ¿Por qué está aquí si no parece que se utilice en ninguna parte?

De hecho, es muy posible que no esté cubierto por tests, lo que hará bajar el índice de cobertura. Esta métrica en medida absoluta no es muy significativa. Sin embargo, sus variaciones son muy importantes para valorar la salud del proyecto. Si un conjunto de cambios hace bajar la cobertura de tests tenemos un problema.

Por otro lado, desde el punto de vista de desarrollo lean, introducir prestaciones que no se necesitan provocará dificultades en las siguientes iteraciones. Es posible que el feedback recibido nos indique que esa prestación que habías añadido se confirme irrelevante, o que haya que implementarla de una manera diferente. Tendrás que deshacer el trabajo y verificar que eso no provoca efectos inesperados.

Posiblemente, la mejor forma de practicar para evitar caer en YAGNI sea Test Driven Development. TDD nos requiere añadir únicamente el código de producción necesario para pasar cada test, ni más, ni menos. El riesgo aquí es introducir código que no sea usado por los tests ya creados. La única forma de detectar ese código es ejecutar los tests con coverage. De este modo puedes identificar las líneas que no reciben ninguna llamada y borrarlas.

Si no tienes una buena cobertura de tests es más complicado identificar el código que te sobra. Las herramientas de análisis estático pueden ayudarte en esto. Un buen IDE te alertará de código al que no se llama nunca, pero únicamente una suite de tests que verifique todo el comportamiento de tu aplicación te dará seguridad para hacerlo.

En resumidas cuentas, es preferible que nunca añadas más código del que necesitas.

Otra cosa distinta es que dediques tiempo a realizar refactors preparatorios. Una vez que has terminado una historia tiene sentido dedicar un tiempo a refactorizar el código y dejarlo en mejor estado para el futuro. Pero esto no es añadir funcionalidad, que es de lo que nos previene YAGNI.

II Prácticas

En esta parte hablamos de diversas prácticas relacionadas con el desarrollo de software.

16 Cómo funciona TDD

Voy a intentar explicar por qué funciona Test Driven Development. No cómo se hace, sino más bien los mecanismos que hacen que partiendo de un test, se pueda desarrollar código de producción.

Como sabemos TDD consiste en escribir primero un test que describa un comportamiento que deseamos implementar en software. Un test, en realidad, no es otra cosa que un programa que invoca una unidad de software y verifica que se ha generado un resultado específico.

Entendamos unidad de software algo que se puede ejecutar, independientemente de su tamaño o su forma. El resultado puede ser tanto una respuesta de esa unidad como un efecto en alguna parte. Para simplificar, imaginemos que la unidad es una función o un método de un objeto.

Según la 1ª ley de TDD no se escribe nada de código de producción sin un test previo que falle. Así que sea cual sea el primer test siempre fallará porque no hay código que se pueda compilar o interpretar.

Obviamente, el primer requisito para que un código se ejecute y tenga un comportamiento observable es que exista. Y el requisito para que un test se ejecute es que la unidad bajo test se pueda importar, instanciar y ejecutar.

Por ese motivo, el primer test no debería ir más allá de requerir la existencia del código de producción y no más que lo exigido por el lenguaje para que el test pueda correr. Para conseguir esto tienes que añadir código hasta que el test deje de fallar.

Los tests pueden fallar por dos tipos de motivos:

  • errores relacionados con la compilación: el código tiene que poder compilarse/interpretarse y ejecutarse.
  • errores relacionados con el comportamiento: el código puede ejecutarse pero no realizar el comportamiento deseado.

Los errores relacionados con la compilación tienen que ver con todo lo que es la sintaxis del lenguaje, pero también podrían tener que ver con detalles de uso de frameworks, configuraciones, etc. (esto si estamos haciendo outside-in TDD, por ejemplo, testeando contra un endpoint)

Tenemos que corregir esos errores hasta hacer que el test falle por la razón correcta: que el código de producción no implementa el comportamiento.

Este es el punto clave de todo. En TDD un test debe fallar solo porque aún no existe un código en producción que lo haga pasar.

La 2ª ley de TDD nos dice que este test debería ser lo más pequeño posible. Una forma de asegurarse es que el test solo pueda fallar por una única razón. Esto puede ser difícil de entender al principio.

Si no hay código, la razón por la que fallará el test es porque no existe la unidad bajo test. Por eso, en ese primer test, no deberíamos hacer aserciones sobre comportamiento. Solo cuando tenemos la seguridad de que la unidad es ejecutable las introducimos.

El test, entonces, sería una especificación formal de comportamiento de la unidad bajo test, por lo que la tarea de desarrollo consiste en introducir el código que haga pasar ese test (sin hacer fallar cualquier otro test existente sobre la misma unidad).

Además de formal, el test es una especificación operativa: nos dice cómo tiene que comportarse la unidad a través de su ejecución en un cierto contexto (que incluye los parámetros que recibe la unidad, etc).

Hay muchos códigos posibles que podrían hacer pasar el test. Por ejemplo, si testeo la función sum(a, b) para sum(2, 3) == 5, me bastaría devolver 5 (sin realizar ningún cálculo) o hacer bucles for contando y acumulando. Da igual mientras el test pase.

Entonces tenemos la 3ª ley, que nos dice que solo añadimos el código de producción suficiente para hacer pasar el test. No más. Así que entre las infinitas posibilidades escogemos la más sencilla, que muchas veces es devolver la respuesta tal y como se espera en el test.

Esto provoca que el test pase, estableciendo la línea base de comportamiento de la clase en ese escenario. Una vez que tenemos esto, es cuando podemos introducir refactoring porque el comportamiento está definido (aunque muy limitado)

En el ejemplo anterior, nuestro código es correcto para todas las sumas de dos números que den 5 como resultado. Fallará en todas las demás, porque no hay código que se encargue de ellas.

Sin embargo, para que el algoritmo pueda cambiar, incluso aunque sepamos de antemano cómo es ese algoritmo, tenemos que introducir nuevos ejemplos en forma de tests que provoquen la necesidad de introducir los cambios requeridos en el algoritmo.

En nuestro ejemplo de la suma, tendríamos que introducir un nuevo test con datos de ejemplo que sumados NO den 5, ya que es la manera de hacer que el test falle. Si introdujésemos un test para una suma de resultado 5 pasaría, lo que NO nos aporta información para añadir código de producción, porque el test verifica el mismo comportamiento que la unidad de software ya implementa.

Si introduces un ejemplo como sum(2,4) = 6 estás cuestionando la implementación existente, lo que obliga a introducir algún cambio. Se podría decir que al añadir un test estás haciendo la hipótesis de que la unidad de software implementa un comportamiento.

Como el test falla, la hipótesis no se verifica y tienes que cambiar el código de producción para conseguirlo. De nuevo, da igual el código que escribas si:

  • el test nuevo pasa
  • todos los tests anteriores siguen pasando

Cuando el nuevo test pasa (junto a los anteriores) se ha establecido una nueva línea base de comportamiento. La unidad bajo test es capaz de cubrir más casos. Esto te permite refactorizar, ya que el comportamiento (hasta ahora) está definido y eres libre de cambiar el código siempre que se mantengan los test funcionando. Los test que pasan garantizan el comportamiento, si dejan de pasar, es que has eliminado parte de ese comportamiento. Los test de TDD se convierten en tests de regresión una vez los haces pasar.

El ciclo se repite hasta que no puedes imaginar un nuevo test que pueda cuestionar la implementación existente.

Esto es posible también porque los tests son especificaciones replicables: puedes ejecutarlos una y otra vez obteniendo los mismos resultados.

Así que, en resumen, se podría decir que TDD consiste en someter a prueba una unidad de software para ver en qué falla, de modo que el test nos diga exactamente qué es lo que necesitamos hacer.

Incluso aunque no exista código.

17 TDD no son tests, son ejemplos

Estoy buscando formas nuevas de explicar lo que es TDD, Test Driven Development. El mayor problema es, por supuesto, que el nombre no ayuda. Cierto que TDD consiste en escribir primero un test que defina el comportamiento que queremos desarrollar usando un ejemplo del mismo.

Por esa razón, algunas personas proponen usar la expresión Specification by example (Especificación mediante ejemplos) que captura mejor lo que hacemos en el proceso TDD: poner ejemplos del comportamiento deseado especificando el contexto, los datos de entrada y lo que esperamos que suceda como consecuencia del comportamiento, ya sea una respuesta o un side effect.

El hecho de usar Test en el nombre también tiene el efecto negativo de que mucha gente piensa en lo difícil que resulta a veces testear su código. Precisamente, es difícil porque ese código no está diseñado para ser testeado, probablemente requiere mucha preparación, un smell a la hora de hablar de tests, y puede que hasta sea imposible ponerlo bajo test tal como está.

Eso se previene haciendo TDD.

En fin. Como se ha dicho muchas veces TDD no es testing, sino desarrollo y diseño, aunque la herramienta que utilizamos sea la misma: los tests automáticos. Y lo que debe hacer un test de TDD es mostrar un ejemplo del comportamiento.

En este punto se plantea el problema de cómo tienen que ser esos ejemplos. Y aquí viene la típica historia de empezar por el caso más sencillo. En realidad, lo correcto sería decir algo así como el ejemplo más sencillo. Caso es terminología de testing.

Tampoco tengo claro que decir el más sencillo sea un buen consejo. A decir verdad, cualquier ejemplo nos valdría para el primer test porque nuestro primer código de producción va a ser devolver una constante. Los siguientes ejemplos son los que moverán el desarrollo.

Sin embargo, si prevemos que ciertos ejemplos van a ser más fáciles de desarrollar puede ser buena idea empezar con ellos. Pero en realidad, lo que nos interesa es desarrollar el siguiente comportamiento que nos interese más, por razones de negocio/dominio.

Una cosa que se suele pasar por alto es cuál es la función de cada fase del ciclo de TDD.

La primera es el test que falla. Esta fase define el comportamiento que queremos alcanzar para un ejemplo concreto. Este ejemplo representa un comportamiento que no existe aún y que nos interese implementar a continuación, posiblemente por razones de negocio.

El test adecuado debería construirse de una forma trivial: instanciar el componente bajo test, utilizarlo con los datos de ejemplo y verificar que la respuesta es la que se espera.

La siguiente fase es añadir el código de producción necesario para que el test que fallaba, pase. Esta fase busca establecer la línea base de comportamiento hasta ese momento. No implica implementar el algoritmo completo, solo lo suficiente para tener el nuevo comportamiento a la vez que se mantiene el que ya existía. Es decir, se trata de poner el test en verde cuando antes y establecer la red de seguridad para la fase de refactor. En este punto nos bastaría devolver la respuesta esperada tal cual o abrir una condicional para un ejemplo.

La tercera fase es la de refactor. En TDD clásica esta fase busca introducir o mejorar el diseño de la solución mediante la identificación de oportunidades de generalizar un algoritmo para los ejemplos de que disponemos en el test, sin preocuparse de los que puedan venir después.

Esto se puede hacer aplicando las técnicas de refactoring comunes, buscando los smells generados por las implementaciones simples que hacemos en la segunda fase. Es fundamental mantener todos los tests existentes pasando, lo que garantizará que mantenemos el comportamiento.

Así que en resumen, TDD o especificación mediante ejemplos es:

  • Ilustrar mediante un ejemplo el comportamiento que deseamos implementar a continuación, para lo que usamos un test.
  • Implementar la solución más obvia posible.
  • Eliminar los code smells refactorizando.

Decimos muchas veces que TDD no es testing. Es que no lo es, pues forma parte del proceso de diseño.

Sin embargo, tiene un side-effect interesante al proporcionarnos una batería de tests base. Esta batería necesita cierta limpieza, ya que tras el proceso de TDD pueden quedar muchos ejemplos por caso, un caso se puede representar con uno o más ejemplos, que hacen los tests redundantes y se pueden eliminar.

También se eliminarán algunos tests que han servido para mover el desarrollo, pero no que no aportan nada al proceso de verificación.

Dependiendo de tu enfoque al hacer TDD puedes tener distintos niveles de testing. Por ejemplo, si haces BDD posiblemente con ello tengas un test de aceptación que ejercita el sistema desde fuera. Además de varios tests unitarios.

18 Lo que prueban los tests

Los tests pueden probar la presencia de errores, no su ausencia. (E. Djikstra)

La frase de Dijkstra tiene un problema gordo: ¿cuál es el criterio que define lo que son los errores? Si afirmamos que los tests no pueden probar la ausencia de errores es que existe otro criterio que, obviamente, no son los tests.

Por ejemplo, si construimos una pieza de software con TDD siguiendo estrictamente las reglas de cambios mínimos suficientes, los tests prueban la total ausencia de errores en el dominio de conocimiento definido por esos mismos tests.

Si ese código sale a producción y aparece un error, ¿qué significa? Pues básicamente que el conocimiento necesario no estaba reflejado en los tests y, por consiguiente, no podía estar reflejado en el código.

Este espacio de no conocimiento es donde viven los bugs. Pero ¿dónde reside ese conocimiento completo que nos permite afirmar que un código contiene errores?

Ese conocimiento más completo puede existir o no. A veces tal conocimiento es algo así como vox populi y nadie se preocupa de hacerlo explícito hasta que pasa algo, el bug, y se nos enciende la bombilla.

A veces es conocimiento que alguien tiene pero no comunica. Y otras veces es conocimiento que es completamente nuevo y que solo ha surgido como consecuencia de desplegar un código y encontrarse con ese caso que hace aflorar la laguna.

El problema de escribir software es el problema de representar un conocimiento, incompleto y cambiante, de una forma lo más aproximada posible. Los tests proporcionan una segunda representación que podemos usar para verificar el software, pero si es una representación incompleta o incorrecta, estaremos en las mismas. Habrá un desfase entre el conocimiento necesario y el representado en el código y los tests. Idealmente, los tests y el código deberían representar exactamente el mismo conocimiento.

Por esto mismo es tan importante trabajar en iteraciones pequeñas y baby steps en todas las fases del proceso de desarrollo. Cuanto menos código introduces en una iteración, menos riesgo de que el desfase de conocimiento sea grande. Y más fácil de resolver si ocurre.

Cada iteración nos sirve para evaluar el desfase de conocimiento y si el nuevo código introduce o revela nuevos desfases. Si desplegamos montañas de código en cada iteración, los desfases serán abismales y difíciles de resolver.

19 Bugs

Llevo un tiempo dándole vueltas al concepto de bug. Lo que viene a continuación es muy subjetivo y discutible, como corresponde a este libro.

En principio, existe un cierto acuerdo en definir un bug como un defecto al implementar un programa que proporciona resultados inesperados o mensajes de error y operaciones que no pueden terminar.

Claro que también depende del punto de vista. Todas hemos visto reportes de bugs que relatan la odisea de una usuaria usando mal la aplicación cuando el sistema funciona perfectamente… bueno, podría ser un bug de UX si vamos a eso.

Pero yendo al código, un bug… ¿qué es?

Podríamos definirlo como un defecto del software que provoca resultados erróneos.

Pero, ¿cómo es posible escribir software con defectos?

La pregunta va totalmente en serio. No estoy preguntando: ¿Es posible hacer software sin defectos?:

Ya, bueno… (me parece escuchar) todo el mundo puede tener un mal día y olvidarse de chequear que un divisor pueda ser cero y provocar un error de división por cero.

¡Ajá!…

Dejando aparte los despistes… una cobertura de tests correcta debería detectar esos defectos antes de que lleguen a producción… ¿Cómo es posible no detectar eso? (Obvio: porque no está testeado)

Pero ¿qué es y cómo se consigue que una batería de tests cubra todos los casos?

Pues a veces no se consigue, lo que viene siendo demostrado por nuestros abultados backlogs de bugs, incluso eliminando aquellos en los que la culpa es del cha-cha-chá.

Una cobertura suficiente de tests se consigue teniendo una especificación completa de las prestaciones del sistema y que esta sea traducida a tests.

Esto tiene el problema de que no hay forma de hacerlo sin caer en el waterfall o cosas peores. Solo se me ocurre una manera de conseguirlo: usar metodología BDD y TDD y desarrollar el producto de manera iterativa.

Pero si lo haces de esta manera ocurre una cosa curiosa… dejas de tener bugs. Bueno, no exactamente. En ese caso, los defectos se pueden reconceptualizar como comportamiento no definido. Para resolverlos se escribe un nuevo test que defina el comportamiento que falta, exponiendo el defecto. El código que hace pasar el test, resuelve el defecto.

Yo diría que no es solo una cuestión semántica, sino que también es una cuestión psicológica. Los bugs tienen un componente de culpa: alguien ha metido la pata.

Sin embargo, desde este otro punto de vista, el defecto puede tener dos causas principales:

  • era una situación desconocida que descubrimos al llevarla a la práctica.
  • era un conocimiento implícito, o que dábamos por sabido, que habría que hacer explícito.

Y en ambos casos, me parece algo positivo, ya que es descubrir conocimiento: el que no sabíamos y el que estaba encubierto.

Para lograr esto hay que trabajar en iteraciones e incrementos muy pequeños, mezclando y desplegando frecuentemente. Y acompañando de nuevos tests en cada conjunto de cambios.

Los defectos se esconden en las partes de código que no están protegidas por algún test. E incluso teniendo 100% de cobertura de buenos tests puedes tener comportamientos no especificados que se manifiesten en producción.

La diferencia está en si la ausencia de esos comportamientos ha sido una decisión, como el 20% de funcionalidad que no vas a desarrollar en esta iteración, o ha sido algo inesperado.

20 Code review

Voy a hacer capítulo sobre por qué las code reviews al hacer pull request no sirven para aprender a desarrollar bien. De hecho, las considero contraproducentes en ese aspecto. Y no solo para juniors, sino también para seniors que sean nuevos a un proyecto.

Una asunción es que la persona que hace la petición de code review ha trabajado sola en ese pull request específico. El primer problema que se puede ver aquí es que la revisora, o revisoras, no tiene el contexto ni del problema ni de como se ha llegado a la solución.

De hecho, el que una persona trabaje sola en un equipo es una mala práctica. Pero eso lo trataré en otro momento.

Puesto que los problemas raramente tienen una solución única y en programación hay cientos de formas de hacer algo, la review en estas condiciones puede ser algo tremendamente subjetivo e injusto. Como desarrolladoras nos hemos encontrado muchas veces en la necesidad de aceptar compromisos. Por ejemplo, hacer un código no muy limpio para no eternizarnos refactorizando cosas que no tienen que ver con la historia que tenemos en la mano en ese momento. Diréis: pues documéntalo con un comentario. Y yo digo: la revisora no va a leer el comentario.

Pero bueno, el punto es que hay algunas decisiones que parecerán erróneas a la revisora porque le falta el contexto. Incluso cuando esas decisiones las ha tomado alguien de sobrada seniority. Hablando de lo cual…

El caso de persona junior que pide la review y se le tira casi todo para atrás porque ha hecho varias cosas mal según las revisoras. Pues esto es una mala práctica atroz. En primer lugar, porque convierte la code review en un examen de programación que, además, es injusto.

Es injusto porque a la junior le falta información. Ojo, no digo formación, digo información. De hecho, si una junior llega a una review en estas condiciones no es que le falte información, es que no se le ha dado. Se le está ocultando.

Me explico. Típica revisión: es que no has considerado que nosotros no lo hacemos así porque…. En primer lugar, si hay condicionantes, compromisos o requisitos para hacer las cosas: ¿por qué no se ha ayudado a la persona junior a conocer esa información antes?

Por otro lado, está el cambio de contexto al hacer la review. Tú estás en otra cosa y tienes que cambiar tu contexto de un problema a otro, y tratar de entender que está pasando en ese otro contexto. Pero es que en la daily ya coordinamos y sabemos en qué está cada quién… Ya.

Por no hablar de lo que ocurre mientras la review no se lleva a cabo. La solicitante: ¿tiene que pararlo todo y esperar a recibirla? Porque entonces también sufrirá el cambio de contexto, más el marrón de integrar los cambios que haya habido en paralelo.

Y eso contando con que no se hayan introducido conflictos, que también puede pasar.

¿Solución a esto? Pues programación colectiva, pairing o mobbing. ¿Por qué? En primer lugar, porque compartimos el contexto, aunque vengamos con ideas diferentes. De este modo todas podemos proponer soluciones distintas que se discuten en el momento, en un contexto compartido.

Sirven para mentorizar a personas con menos experiencia, pueden hacer sus propuestas y se discuten en el momento, incluso probándola. Sirven para descubrir y aprender aquellas cosas que no están documentadas, o que lo están en algún lugar que no se actualiza desde hace años.

¿Pero cómo puedes tener dos personas (o más) trabajando en lo mismo? Qué pérdida de tiempo. Pues en realidad es un ahorro de tiempo. Esto del tiempo me hace gracia porque nunca (o casi nunca) se cuenta el tiempo que se pierde en la revisión y en el ir y volver con los cambios.

Es ahorro de tiempo porque en un poco más, y a veces menos, del tiempo en que se completa una historia, se hace con menos problemas y menos frustraciones. Además, todas las personas que participan aprenden más, lo que las hará más productivas en el futuro inmediato.

Una posible ventaja de las code review es cuando un cambio puede afectar a un entorno mayor que el equipo. Con todo, en ese caso puede que sea más productivo e interesante invitar a otros implicados a una sesión de mob programming de modo que los problemas se puedan prevenir y evitar de una manera más eficaz y precoz, en lugar de tener que parchear o volver atrás a la causa raíz con el pull request hecho.

Por otro lado, en las sesiones de mob programming puede, y debería, participar gente de producto, clientes, etc. Igual no en todas o todo el tiempo, pero sí cuando se tratan reglas de negocio, UI, UX, etc. o no se comprende bien el problema en el que se está trabajando.

¿Las mob programming en remoto son complicadas? Un poco, sí. ¿Que a veces hay partes que realmente no haría falta hacer en mob? También, pero gracias a haber hecho mob tenemos mejor definidas y delimitadas esas partes y de todos modos las integraremos durante una sesión de mob.

Como ves, no se trata de eliminar las code reviews because yes, sino entender que introducir prácticas, propuestas hace décadas, por cierto, de trabajo en equipo permitirán extender el conocimiento de forma más eficaz y sólida a través de un aprendizaje continuo en lugar de hacer evaluaciones en forma de code review en las que realmente no se aprende nada y actúan más de barrera que de feedback constructivo.

Sí, sí, ya sé que tú haces las code review muy bien. Yo, siendo sinceros las hago fatal.

Por no mencionar los proyectos que tienen code reviews y están plagados de bugs y problemas igualmente, demostrando que la code review no es garantía de nada.

O por no mencionar el problema de las code reviews de muchísimos archivos que es humanamente imposible hacer bien, o las que solo se ocupan del code-style. O las que llevan 3000 approves porque eran dos líneas.

En resumen, para aprender a desarrollar bien lo que hay que hacer es programar juntas.

He debido pinchar en hueso con esto de las code reviews[^hilo]. Intentaré contestar algunas de las ideas que han salido y matizar el texto anterior. Prefiero hacerlo así, que es más fácil enlazar un argumento largo. Vamos a ello.

[^hilo]; El capítulo nace de un hilo de twitter que generó una cierta discusión posterior y quedan algunas referencias.

En primer lugar, el punto del argumento es que las code review no sirven para aprender y mucho menos para que alguien junior aprenda. De hecho, ni siquiera son buenas para entender un proyecto grande, necesitas otras estrategias de aprendizaje.

Dicho esto, las code review se supone que sirven para garantizar la calidad del código, evitar errores, particularmente de integración, y aumentar la confianza en el código que se mezcla y se despliega.

No he entrado en ese punto particular, aunque mi opinión es que las pull request con code review no tienen mucho sentido en el contexto de equipos dentro de una empresa, sí en open source y tal. ¿Por qué digo esto?

Porque todas las ventajas que se señalan aumentan su eficacia si se hacen antes, no a posteriori. Las pull request con code review aumentan el trabajo en progreso (WIP). De hecho, son constraints de libro. La definition of done debería ser feature que el usuario puede usar.

La asunción básica que estamos haciendo es que las PR se hacen porque trabajamos individualmente. Hablamos de equipos, pero en muchos casos el trabajo concreto se realiza en soledad y es después de finalizado cuando se hace la review. Esto tiene varios problemas.

Un punto sugerido como positivo es que alguien que no esté tan metido en el problema como tú ayuda a tener menos bias. Para empezar: ¿Por qué una persona debería estar trabajando en un problema sola? Si sabemos que va a haber sesgo, ¿por qué no estar dos o más trabajando y prevenir ese bias desde el inicio?

También se menciona la capacidad de la Code Review para abrir debate y llegar a acuerdos sobre como escribir software. Pero en la Code Review el código ya está escrito. ¿Por qué no mover ese tipo de debates al principio de los proyectos y durante el desarrollo?

Estoy de acuerdo en que una forma de trabajo no debe ser reemplazada por la otra porque sí y sin tomar medidas adecuadas, pero ese no era el punto del capítulo.

Para mí el feedback en las code review llega tarde y mal. El editor está en modo acabar la tarea y ha invertido tiempo y esfuerzo (coste hundido que le llaman), lo que hace que la review sea más vista como una evaluación que como una contribución.

Por otro lado, ¿por qué alargar el tiempo de feedback cuando puedes hacerlo inmediatamente? Hay un gráfico sobre los ciclos de feedback (de Kent Beck) que es muy ilustrativo sobre esto. Ensemble-programming proporciona feedback instantáneo en el momento requerido.

Y ya que mencionamos al equipo: pero es que no es tu código, es el código del equipo… Entonces, ¿por qué no lo escribe el equipo en conjunto en lugar de hacer que una developer lo haga en solitario para que el equipo juzgue si es aceptable o no?

21 Refactor (sin piedad)

Hablemos de Refactor Mercilessly o Merciless Refactoring. Una de las prácticas de Extreme Programming.

Para empezar, creo que muchos equipos confunden refactor con reescritura. Vale, que sí, que son bastante sinónimos… Yo defino refactor como pequeños cambios inocuos, sin riesgo. Reescritura la entiendo como cambios muy grandes, con frecuencia en nivel de arquitectura.

Refactor mercilessly, refactorizar despiadadamente, consiste en hacerlo frecuentemente, continuamente y sin pedir permiso.

  • Frecuentemente quiere decir varias veces al día.
  • Continuamente quiere decir todos los días.
  • Sin pedir permiso quiere decir que no es una tarea planificada, ni importa quién escribió la línea a refactorizar.

– Pero Fran, ¿qué salvajada es esa?

Todo esto se relaciona con la regla del campamento y el WTF factor. Vayamos por partes:

La regla del campamento reza: deja el código por el que pases mejor de lo que estaba.

Pasamos la mayor parte del tiempo leyendo código. Para implementar cualquier cosa nueva o para solucionar un problema tenemos que leer código. Incluso después de escribir código tenemos que releerlo.

Si encontramos algo que no se entiende dedicamos un rato a pensar en ello. Ese rato puede ser muy largo. Puede que no haya nadie a quien preguntarle. O sí, pero ya se ha olvidado. Puede que ese código lo escribiésemos nosotras mismas.

Ese es el WTF factor: ese trocito de código que nos parece raro, incomprensible, contradictorio… Al llegar el momento ajá! y entender aquello tenemos dos opciones:

  1. Seguir adelante, dejarlo como está y quedarnos con ese conocimiento en la memoria.
  2. Hacer un pequeño arreglo seguro, ya sea por tests o por refactor automático, commit y push.

La opción 2 es la buena.

Habremos ahorrado al developer del futuro un tiempo importante para otras cosas. Habremos mejorado la velocidad del equipo en el futuro. Habremos reducido un poco la deuda técnica.

El tiempo de hacer este refactor puede ser de minutos: cambiar el nombre de algo, extraer un método, ordenar unas líneas… Es una pequeña inversión con alto retorno.

Para hacer esto, son necesarias algunas condiciones. Estos refactorings tienen que ser seguros por lo que:

  • Debería haber un test cubriendo esa área de código.
  • Si no lo hay, el refactor debería ser automatizado con el IDE
  • El scope es limitado, como ser interno a una función

Si lo que cambia son interfaces públicas el riesgo aumenta un poco, pero teniendo tests que cubran ese cambio, no deberías encontrarte con problemas, aunque es posible que el volumen de cambios sea grande. En ese caso, hay estrategias para ir progresivamente, de modo que reduces el riesgo. Por ejemplo, añade un nuevo método con la nueva signatura y úsalo solo en el lugar que provocó el cambio, haciendo que el viejo lo llame por debajo. Luego podrás cambiar los usos del viejo por el nuevo progresivamente.

Aparte, necesitas un compromiso por parte del equipo: ¿cómo es vuestro workflow? ¿Puedes garantizar que el cambio estará en main lo antes posible? ¿O habrá un pull request de dos líneas languideciendo durante días o semanas?

Otro punto es la propiedad del código. En un equipo, el código es del equipo o no es de nadie. Si hay que pedir permiso para hacer un cambio así al autor de la línea tienes un smell en el equipo.

Estos pequeños cambios acumulados nos llevan progresivamente a un código en mejor estado. En un momento dado, nos pueden facilitar un cambio de mayor calado que será más evidente y más fácil de hacer.

Este refactor cotidiano, o refactor oportunista, debería ocurrir constantemente. Contribuye a mantener la deuda técnica bajo control al mejorar la representación del negocio en código, pero también contribuye a un mejor conocimiento del código por parte del equipo.

Pero es muy importante entender que este tipo de refactor, como señalaba al principio, se hace en unidades muy pequeñas, pero muy frecuentes: varias veces al día, todos los días.

Piri is qui isí ni si siqui nidi i pridicciín… Nope. Todo esto se aplica mientras trabajas para sacar las historias de usuario a producción.

22 Test and commit or revert

Hoy me ha dado por practicar un poco el flujo test && commit || revert y me lo estoy pasando muy bien.

Test and Commit or Revert

Por supuesto, es un flujo propuesto por Kent Beck. TCR (Test && Commit || Revert) consiste en que tras cada cambio en el código de producción se lanzan los tests. Si los test pasan, se ejecuta un commit con los cambios.

Pero si los tests NO pasasen, se borran los cambios aplicando un reset duro para eliminarlos. Es decir, si tu cambio rompe los test, se descarta por completo.

Para automatizarlo se crea un pequeño script que realiza todo el proceso. La versión más básica simplemente hace lo que se indica: ejecutar los tests y hacer commit si han pasado o revert si no.

Esta versión simple tiene algunos problemas, porque también borraría los tests nuevos si no pasan. O si haces TDD sería bastante engorroso, ya que, por defecto, se borrarían los tests nuevos al intentar ejecutarlos.

Por esa razón, se han creado algunas versiones tuneadas que, por ejemplo, aseguran que el código compila y que solo se revierten los cambios en el código de producción y no se revierten los tests.

Con eso, se puede hacer TDD con TCR. Y resulta hasta mágico.

Haces un test, ejecutas el script TCR y lo ves fallar. Añades código de producción para hacerlo pasar y TCR de nuevo. Y así hasta terminar. Si metes la pata y un test que pasaba, falla, el código de producción que lo provoca se elimina. Vuelta a empezar.

Con este flujo puedes aprender a refactorizar en pequeños pasos, ya que favorece la parte de make the change easy, manteniendo los tests en verde.

Además, como no querrás perder mucho código de una vez, te fuerza a añadir muy poquito código de producción. Si lo pierdes que sea poco.

Este flujo complementa a TDD en la parte de refactor, puesto que se parte de que los tests están en verde y se trata de mantenerlos en verde. Y puede ser una buena base para hacer refactor en código legacy una vez que hayas introducido tests (por ejemplo, mediante Approval tests)

III Patrones

En esta sección nos centraremos en los patrones GRASP.

Hablamos mucho de SOLID, pero muy poco de GRASP. Es otro acrónimo que representa un conjunto de patrones o heurísticas para definir el reparto de responsabilidades de un sistema de software orientado a objetos. Nos ayuda a responder a preguntas mucho más básicas que SOLID.

Por cierto, GRASP es el acrónimo de General Responsibility Assignment Software Patterns y los publicó inicialmente Craig Larman. Estos patrones son herramientas mentales para el diseño de software orientado a objetos.

23 Controller

La verdad es que explicar los patrones GRASP en tuits es bastante complicado, porque en algunos casos se tarda menos con un ejemplo de código. Esta vez le toca el turno a Controller, que se puede explicar de forma bastante sencilla sin necesidad de ejemplos de código.

Controller responde a la pregunta: ¿por dónde se entra al sistema? Y básicamente lo hace introduciendo un objeto (el controller) que actúa de intermediario entre la capa de interfaz de usuario y la aplicación, vinculando eventos que suceden en UI con casos de uso en la aplicación. Por supuesto, este remite al patrón MVC que nació en las GUI y se usa habitualmente en aplicaciones web. En principio Controller se refiere a un objeto que es capaz de gestionar un determinado evento de la UI y lanzar el caso de uso correspondiente. También cabe la posibilidad de que un solo controller agrupe varios métodos que responden a distintos eventos de la UI que estén estrechamente relacionados, como los verbos de una API REST para un cierto tipo de recursos.

Esto es, por ejemplo: si el usuario envía un formulario, hay un controlador que captura ese evento (en HTTP una request a una URI específica), extrae los datos, instancia un caso de uso y lo ejecuta, provocando un efecto o recuperando información, y devolviendo la respuesta adecuada.

La tarea del controller es gestionar ese proceso, pero no debe contener lógica de negocio. Tan solo es un traductor entre la intención de la usuaria de la aplicación y el caso de uso que representa esa intención. Si hay lógica de negocio en el controller tienes un problema.

Controller es un elemento de infraestructura y adapta una tecnología concreta. Existen muchos frameworks que nos ofrecen todo lo necesario para montar controladores y, como tales, deberían estar en la capa de infraestructura de la aplicación. Sí, ya sé que Rails, Laravel y un largo etcétera se postulan como la base de las aplicaciones con sus MVC y sus Active Record y acaban mezclándolo todo, siendo habitual encontrar aplicaciones basadas en ellos con lógica de negocio en controladores gordos, pero esa es otra discusión.

Lo importante es quedarse con la idea de que Controller es un patrón para resolver el problema de cómo entrar a una aplicación y cómo ejecutar las intenciones de las usuarias. También, que la única lógica que debería contener es la necesaria para identificar cuál es esa intención y, por tanto, lanzar el caso de uso que le da respuesta. Lo mejor es hacerlos muy finitos y aunque puedas agrupar varias acciones en ellos, es fundamental que estén muy cohesionadas. Es mejor tener muchos objetos controller con pocas responsabilidades, que pocos que hacen de todo.

24 Creator

Tomemos por ejemplo, el patrón Creator…

El patrón creador nos ayuda a decidir quién crea ciertos objetos. La responsabilidad será de quien cumpla una de estas condiciones:

  1. Contiene o agrega instancias de la clase a crear: un ejemplo típico es Pedido/Item. El pedido agrega items, por tanto, debería instanciarlos.
  2. Registra instancias de la clase a crear. Por ejemplo: Cliente/Pedidos.
  3. Usa instancias de la clase a crear. Por ejemplo: Cliente/Dirección de entrega.
  4. Contiene la información necesaria para instanciar objetos de la clase a crear: en general, si un objeto tiene información para crear otros objetos puede ser su creador. Esto nos lleva a las distintas implementaciones de patrones creacionales.
New no, lo siguiente.

Se podría decir entonces que un objeto que tenga mucho interés en otro, y que sepa mucho sobre él, puede ser creador de este segundo tipo de objetos.

Aparte, los objetos agregadores (en DDD serían los agregados) deberían crear esos objetos internamente. Es decir, no se les pasan los objetos agregados ya instanciados, sino los datos que puedan necesitar para construirlos.

Normalmente, esto es porque tales objetos agregados no tienen vida fuera de la agregación. La idea de una línea de factura independiente de una factura no tiene sentido. Por tanto, no creas líneas y luego se las pasas a la factura. Le pasas los datos de la línea.

Por otro lado, es muy conveniente que haya un solo lugar para la creación de cada tipo de objetos. Más allá de su función constructora, si la creación de un objeto implica algún tipo de lógica que no se puede encapsular en ella, introduce un builder o un factory que lo haga.

En resumen: la responsabilidad de creación de objetos pertenece a sus contenedores o agregadores o aquellos que tengan el conocimiento necesario.

25 Information Expert

Otro de los patrones GRASP es el Information Expert. Sirve para responder a la pregunta de ¿quién debería ocuparse de esto?. Y la respuesta es: pues el que sabe de ello, el experto.

Otra forma de verlo: ¿qué objeto debería ser el que responde a este mensaje?

Pienso que es un patrón que podemos vincular a Tell, don’t ask: si tienes que preguntarle a un objeto por su estado para tomar alguna decisión que afecte al objeto, ese comportamiento pertenece al objeto que no deja de ser el experto en ese tema.

¿En qué es experto un objeto? Pues depende del concepto que representa y de las propiedades que componen su estado. Aparte, un objeto debería ser experto en su propia consistencia interna, y ser capaz de inicializarse válido y proteger sus invariables.

Es muy fácil verlo con el clásico ejemplo de Pedido/Items. ¿A quién le preguntamos sobre el importe total? Pues al Pedido, porque al contener los Items puede sumar los importes de cada ítem. Y cada ítem, por su parte, podrá calcular su importe porque sabe de su precio unitario y de su cantidad. Además, un pedido sabe que necesitará un cliente y dirección de entrega y facturación. También, puede que haya un tope en el número de items que se pueden pedir. O tal vez, los gastos de envío son gratis si se supera un cierto importe lo cual es algo que Pedido puede decidir. Para algo es el experto… en pedidos. Si no tiene toda la información necesaria sabrá que está incompleto o que no es un pedido válido. Saber eso es su trabajo.

Su anti-patrón suele estar relacionado con los modelos anémicos: el objeto tiene la información, pero no hace nada con ella porque no tiene comportamientos y son otros objetos los que saben qué hacer con ello. Si separas Información y experto entonces no estás haciendo OOP.

¿Te parece un patrón obvio? Pues no veas la cantidad de bases de código que no lo usan o no lo usan suficientemente. Es bastante típico si provienes de un estilo procedural, ya que tiendes a ver los objetos como tipos que contienen datos, que son manipulados por funciones.

Pero la potencia de la programación orientada a objetos reside en que los objetos sean expertos en lo suyo, que solo tengas que pedirles que hagan cosas e interactúen sin tener que supervisarlos, en el sentido de verificar a cada paso que las cosas son correctas.

Ojo. Dar este salto puede ser difícil. En programación procedural importa el conocimiento global del sistema y su control. En programación orientada a objetos el conocimiento está distribuido en objetos que interactúan y colaboran para realizar el trabajo y tienen sus propias parcelas de control. Son dos formas de ver la programación. No son incompatibles, pero parten de puntos de vista diferentes.

Así que, en lo tocante a asignar responsabilidades a los objetos, tienes que dárselas al que sabe y confiar en que lo hará bien.

Por cierto, los information experts suelen ser fáciles de testear unitariamente, así que es muy sencillo y eficaz garantizar que están capacitados para realizar su trabajo.

26 Bajo acoplamiento

Low Coupling es otro de los patrones GRASP. Y es que este tema del acoplamiento es más que importante, es fundamental.

Así que primero hablaremos de qué es el acoplamiento, por qué debería preocuparnos y cómo conseguir bajo acoplamiento.

El término se lo debemos a Larry Constantine, al igual que el de cohesión, comentado en otro capítulo. Coupling o acoplamiento es el grado de interdependencia entre dos unidades de software. Por ejemplo, entre dos objetos.

El acoplamiento es algo inevitable si queremos que dos objetos colaboren. Esto es: si seguimos principios como Single Responsibility, Polimorfismo, etc., tendremos objetos pequeños que colaboran. Para que puedan colaborar, tendrán dependencias unos de otros. No existe el acoplamiento cero. Por esa razón hablamos de alto o bajo acoplamiento (o tight/loose — fuerte/débil). En otras palabras: el problema no está en la existencia de acoplamiento, sino en el grado de acoplamiento y en que lo tengamos bajo control.

Si tenemos una clase Consumer y otra Service, siendo así que Consumer usa Service para hacer algo, decimos que Consumer tiene una dependencia de Service y está, de hecho, acoplada a Service. Una forma de medir esto es preguntarse: ¿Cuánto necesita saber Consumer para poder utilizar Service? Cuantas más cosas, más acoplamiento. Cuantas menos cosas, menos acoplamiento. Así, por ejemplo, lo menos que debería saber Consumer son los mensajes que tiene que enviar a Service y parámetros. Supongamos que el mensaje en cuestión es Service::doSomething.

Menos que eso es no poder usar Service. Pero más que eso es incrementar el acoplamiento. ¿De qué conocimiento extra estamos hablando? Pues por ejemplo:

  • Conocer el tipo concreto de Service
  • Saber instanciar un Service
  • Saber encontrar un Service
  • Conocer propiedades de Service

Martin Fowler llama a esto inappropriate intimacy, indicando que un objeto sabe demasiado de otro objeto.

Conocer un tipo concreto de Service significa usar una implementación concreta. Para evitar eso aplicamos la Inversión de dependencias, haciendo que Consumer dependa de una Interfaz.

Saber instanciar un Service ocurre cuando hacemos new de un Service dentro de Consumer. Este tipo de dependencia puede hacer no testable a Consumer. Para evitarlo, usamos el patrón de Inyección de dependencias, vía constructor o vía setter.

Saber encontrar un Service suele implicar que tenemos un smell Service Locator. Esto pasa por acoplarnos al contenedor de inversión de dependencias para pedírselas desde cualquier sitio que nos venga en gana. De nuevo, aplicar la inyección de dependencias correctamente es la solución.

La herencia genera el mayor acoplamiento posible, ya que no puedes usar una clase sin sus ancestros, por eso hay que tener mucho cuidado cuando decidimos usar el mecanismo de herencia. Debería limitarse a especializaciones, y no para “compartir” comportamiento.

En resumen, para mantener el acoplamiento bajo entre dos objetos hay que:

  • Inyectar las dependencias: no instanciarlas dentro de otros objetos. Ojo a la distinción entre objetos newables e injectables1.
  • Aplicar el principio de Inversión de Dependencias y depender de las interfaces, no de las implementaciones.
  • Mantener aisladas las dependencias dentro del objeto que las usa. Utiliza métodos privados cuando tengas que delegar en la dependencia, de modo que no aparezcan menciones a ella en ninguna otra parte del código.
  • Nunca, pero nunca, inyectes el contenedor de inyección de dependencias. Nunca, never, ni se te ocurra.
To new or not to new

¿En qué nos beneficia el bajo acoplamiento? El código con bajo acoplamiento es fácil de testear, ya que todos los colaboradores de una clase son fácilmente reemplazables por dobles en tests unitarios o fakes en tests de integración.

Es fácil de extender por las mismas razones. Hay pocos puntos en los que tocar en caso de necesitar introducir nuevos comportamientos. Es más posible que solo tengas que añadir código, en lugar de tener que borrar o modificar. El bajo acoplamiento facilita cumplir open/close.

Es incluso más fácil detectar los errores y dónde se producen porque si ocurriese uno seguramente podrás identificar la pieza de software que falla o aquella que controla esa fallo. En una situación de acoplamiento, el error se produce en Consumer aunque sea de Service.

El acoplamiento es especialmente peligroso cuando la dependencia es con módulos de software que no controlamos, particularmente vendors. Para usarlos deberías aplicar siempre inversión de dependencia, introduciendo patrones de indirección, como Adapter.

Si te acoplas a vendors, en caso de que estos cambien, tu aplicación se verá afectada. Es posible que para evitar esos efectos decidas quedarte en una versión concreta de ese veedor, lo cual introduce riesgos de seguridad, de finalización de mantenimiento, de performance… es MAL.

Pero el MAL, ¿eh? Sufrimiento, llanto y rechinar de dientes.

Así que, ya sabes: procura que tus objetos sepan lo mínimo de los objetos de los que dependen. Depende las interfaces, inyecta las dependencias, mantenlas aisladas, usa patrones de indirección.

Mantener a raya el acoplamiento es salud. La de tu aplicación y la tuya.

27 Acoplamiento al estado global

Pues llevaba todo el día peleándome con una variable estática (ni siquiera una propiedad) en una clase PHP usada para evitar un acceso a disco. Un ejemplo de optimización prematura violando principios OOP… TODO MAL. Lo que unido al capítulo sobre acoplamiento me da excusa para hacer otro capítulo, esta vez sobre el acoplamiento al estado global.

¿A qué me refiero con estado global? Pues no a la ONU precisamente, sino a aquello que es visible o accesible desde cualquier lugar del código de la aplicación. Y esto en OOP es un NO.

Es un NO porque es lo opuesto a la orientación a objetos, un paradigma en el que básicamente se trata de ocultar el estado en los objetos. Por otro lado, si hay acceso al estado global el comportamiento puede ser impredecible, dado que cualquier objeto podría cambiarlo sin que se enteren los demás.

Aún más: el estado global puede estar definido y ser cambiado por la propia máquina que alberga el programa. No necesariamente por el programa. ¿Nos vamos haciendo una idea de los problemas que esto puede generar?

Estado global es, por ejemplo, el reloj del sistema. Cada vez que instancias un objeto de hora o fecha estás usando una dependencia global. Lo mismo si empleas el generador de números (pseudo)aleatorios, o algo que esté en el sistema de archivos. Aparte de posibles variables o parámetros globales de tu programa, y cualquier cosa compartida, como una base de datos para tests compartida.

En general, esto hace que el comportamiento de la aplicación sea impredecible o, cuando menos, que no puedas confiar 100% en ella.

También se consideran globales las llamadas estáticas. El patrón Singleton debe su mala fama a sus implementaciones estáticas más conocidas. Y es que se pueden tener singleton no estáticos.

Por ejemplo, los tests. ¿Has hecho tests con cosas que usan fechas o números aleatorios? Ha dolido, ¿verdad? Trucos como poner una fecha muy lejos en el futuro para que ciertos tests pasen. Y luego resulta que el futuro llega y hay que cambiar el test.

O los tests sobre el sistema de archivos, que si fallan dejan sucio el entorno de CI, falseando otros tests o bloqueando el pipeline. O tests que pasan si se ejecutan solos y no pasan si se ejecuta la suite.

Del mismo modo que los tests sufren estos comportamientos impredecibles, la aplicación también los puede tener. El acceso al estado global puede crear dependencias muy difíciles de detectar o errores puntuales muy difíciles de encontrar.

Imagina un objeto que crea un archivo para almacenar una información temporalmente y poder recurrir a ella más tarde, pero hay otro objeto que elimina o escribe ese mismo archivo. Problemas garantizados.

¿Cómo se soluciona esto? Pues en primer lugar evitando acceder al estado global en cualquier momento y lugar. Si necesitas un parámetro de configuración, pásalo a los objetos en construcción o a través de algún sistema controlado si lo necesitas en otro momento.

Si tienes que acceder a estados globales usa patrones de indirección, de modo que un estado global sea representado por un objeto del sistema. Por ejemplo: un TimeService o ClockService que abstraiga el reloj del sistema.

Y cuando necesites un objeto de Tiempo pídeselo solo a ese servicio. De este modo, en testing puedes usar un doble y nunca más tener tests que dependan del día o la hora de su ejecución. Lo mismo para un generador de números aleatorios.

No hay problema que dentro uses la implementación nativa del lenguaje, pero tampoco hay problema en que uses otras implementaciones: patrón Adapter y a tirar. Esto no te evita posibles problemas de tu máquina, pero evita el acoplamiento directo entre objetos dispares.

Así que, en general, es recomendable que cualquier estado global tenga una representación en un objeto de tu aplicación en lugar de acceder directamente a él. De hecho, extendiendo la idea podría decirse que cualquier cosa que no sea tu aplicación y te proporcione información es estado global (hum… ¿Son las usuarias estado global?)

En cualquier caso, representándolo en objetos y usándolos de forma rigurosa tu aplicación será más confiable y más fácil de poner bajo test. De hecho, será más fácil llegar a cambiar en algún momento su implementación por mejores soluciones.

Puede que te preguntes: pero si esos objetos están acoplados al estado global, entonces, ¿en qué quedamos?

Por supuesto. Recuerda que no puede haber acoplamiento cero. Pero en OOP si le damos a un objeto la responsabilidad de proporcionarnos una información específica de ese estado global estamos estableciendo un punto único y controlado de acceso al mismo.

El resto de objetos no se acopla al estado global sino indirectamente. Puedes reemplazar ese objeto con otro que cumpla el mismo contrato y la aplicación no se verá afectada. Ejemplo: un doble de test, pero podría ser otra cosa.

En la introducción de este paper, Beck y Cunningham hablan sobre lo difícil que es cambiar de la mentalidad procedural, pendiente del estado global, a la orientación a objetos, que no considera el estado global.

Teaching object oriented thinking

Controlar el acoplamiento es uno de los puntos clave de la buena orientación a objetos.

28 Alta cohesión

El reverso de low coupling es high cohesion, otro patrón GRASP y otro concepto acuñado por Larry Constantine.

Cohesión nos da una idea de cuan fuertemente relacionados están los elementos de un módulo de software.

Nota previa: módulo de software es una medida un tanto flexible. Para el caso de nuestro estudio de la cohesión lo que digamos se puede aplicar a una clase o a un conjunto de ellas que forman un módulo.

Como tantas otras cosas en el diseño de software una buena forma de analizar la cohesión es ver cómo cambian las cosas. En este caso si cambian juntas o no. Aunque hay algunos matices, porque también depende de lo cercanas que estén las cosas que cambian o si nos perjudica.

Así, las cosas que cambian juntas deberían estar juntas. Es la primera heurística de la cohesión. Esto es: si cuando cambiamos una clase tenemos que cambiar otra, posiblemente ambas pertenecen al mismo módulo. Incluso podría ser que debieran fusionarse.

Por supuesto, siempre respetando otros principios como Single Responsibility, etc. Dicho de otro modo, tienes alta cohesión cuando al realizar cambios en el software estos afectan al menor número de elementos. Idealmente solo a uno.

Cuando dos cosas alejadas entre sí cambian juntas puede ocurrir que estén acopladas, pero no quiere decir necesariamente que tengan que estar juntas (no son cohesivas). Esto es: tenemos que preguntarnos si el cambio es por acoplamiento (malo) o porque están separadas cosas que van juntas.

Obviamente, si un objeto tiene que cambiar porque otro lo hace y pensamos que no debería ser así, es un caso de acoplamiento. Como hemos visto, tendríamos que introducir patrones que reduzcan el acoplamiento directo.

En el segundo caso, tenemos que refactorizar para incrementar la cohesión.

¿Y si metemos todo el código en un sólo objeto?

Pues no.

Hay dos maneras de lograr alta cohesión:

  • Poniendo juntas las cosas que deberían estar juntas (porque cambian juntas)
  • Separando las cosas que no cambian junto con las otras.

En cierto modo, la cohesión es también el arte de saber decir no. Si tenemos una clase en la que unos métodos cambian juntos, pero otros no, es muy posible que tengamos varias responsabilidades y que debamos separarlas.

Es como decir que la alta cohesión y la responsabilidad única van de la mano.

Si tenemos una clase que expone muchos métodos, va contra Segragación de Interfaces (también SRP) y, por tanto, necesitamos “partirla”. Otro principio que va de la mano de la alta cohesión.

Y lo mismo con otros muchos principios: seguirlos nos conduce a tener alta cohesión. Buscar la alta cohesión nos ayuda a respetar los principios de diseño.

Lo mismo cuando buscamos reducir al máximo el acoplamiento. Es un círculo virtuoso.

29 Indirección

Indirection es uno de los patrones GRASP que más nos puede ayudar en hacer software a prueba de futuro. ¿Cómo? Ayudándonos a evitar el acoplamiento directo entre objetos de modo que ambos puedan evolucionar separadamente. Vayamos poco a poco.

Es más fácil ver el valor de este patrón cuando necesitamos implementar algo usando algún vendor específico. Imagina que tienes que acceder a una API externa. Lo típico es emplear alguna librería que te ofrezca un cliente HTTP. Así podrías tener un APIClient basado en el cliente de la librería, que llamaremos VendorHTTPCLient.

La forma de usar VendorHTTPClient con APIClient, podría ser:

  1. Herencia: Haces que APIClient extienda VendorHTTPClient. Mal asunto: la herencia es el máximo acoplamiento. Si VendorHTTPClient cambia, APIClient tiene que cambiar. Si APIClient tiene que cambiar, VendorHttpClient podría no servirte ya. Un Horror.
  2. Composición: APIClient usa VendorHTTPClient como colaborador. Si este cambia, APIClient es relativamente fácil de cambiar. Si APIClient cambia, VendorHTTPClient puede ser sustituido, pero… si OtherVendorHTTPClient tiene distinta interfaz, sigue siendo un percal. Composición es mejor que herencia, pero si la interfaz está definida por el vendor, APIClient sigue estando acoplado. Menos que en la herencia, pero lo bastante como para que sea un trabajo extra de mantenimiento.

La solución es usar indirección. Esto quedaría bien en una camiseta.

En este ejemplo, la indirección consiste en introducir un objeto intermediario entre APIClient y VendorHTTPClient, llamémosle MyHTTPClient. APIClient usará MyHTTPClient (mediante composición) y MyHTTPClient se implementará usando VendorHTTPClient (igualmente por composición).

Si esto te suena a patrón Adapter, es que lo es.

¿Y qué hace MyHTTPClient? Pues:

  • Proporciona una interfaz estable para que APIClient esté protegido de los cambios en VendorHTTPClient. Los cambios serán para MyHTTPClient, por supuesto.
  • Nos permite reemplazar VendorHTTPClient por OtherVendorHTTPClient simplemente cambiando MyHTTPClient. Aún mejor si definimos una interfaz HTTPClientInterface y hacemos que nuestros HTTPClient la implementen usando diferentes vendors.

O para tests podemos crear HTTPClient dummies, stubs o lo que nos haga falta.

Lo importante es que no tenemos que tocar APIClient para nada en caso de que tengamos que cambiar o actualizar el vendor. Podrías decir es que tienes que cambiar el mediador igualmente. Claro. Pero la función del mediador es justamente regular la relación entre los otros objetos, de modo que protege a cada objeto de los cambios del otro. Ambos pueden evolucionar a su aire.

Además del adapter, hay otros patrones que aplican indirection como facade, mediator, o bridge. Por ejemplo, facade es un objeto que simplifica la interfaz de otro objeto o módulo que sea complicado de utilizar. Mediator maneja u orquesta la interacción entre distintos objetos.

El punto clave es evitar el acoplamiento directo, haciendo que el objeto intermediario sea el que tiene el conocimiento para hacer interactuar los otros objetos de modo que la dependencia sea responsabilidad del intermediario que, por otro lado, es controlado por nosotros.

Indirection es una herramienta para lograr Inversión de dependencias. Muchas bases de código problemáticas lo son por tener acoplamiento alto con vendors, por lo que introducir indirección nos ayuda a producir la inversión de control que nos lleva a la inversión de dependencias lo que mejora significativamente la mantenibilidad del código. La indirección también nos permite personalizar vendors sin quedar ancladas en versiones concretas que luego nos impiden actualizar o mejorar otras partes del código.

De hecho, una buena estrategia es introducir el mediador antes incluso de saber qué vendor vas a utilizar. Esto es: cuando sabes que vas a necesitar alguna librería externa, en vez de hacer una dependencia directa, introduce un objeto intermediario que la abstraiga, de modo que cuando lo tengas que implementar no necesites hacer cambios en el consumidor, solo en el mediador.

Así que la próxima vez que vayas a extender un vendor o usarlo en composición, piénsalo dos veces e introduce un objeto indirector. Tu yo del futuro te estará eternamente agradecida.

30 Polimorfismo

Otro de los patrones GRASP es el Polimorfismo. Es la propiedad de la programación orientada a objetos que nos permite enviar mensajes sintácticamente iguales a objetos de tipos distintos. Esto nos permite gestionar variantes de comportamiento basadas en tipos.

¿Y todo eso qué significa? Vayamos por partes. Primero: ¿Cuál es el caso de aplicación del patrón? Imaginemos que ofreces un Service con tres niveles (tiers): free, premium, professional. Una forma típica de modelar esto que Service tiene la propiedad tier (o type, o category…)

Eso implica que en ciertas situaciones necesitarás preguntar a Service por su tier para saber qué precio cobrar, o qué límites de acceso tiene, etc., etc. Eso son muchos if o switch en muchos lugares del código. Incluso respetando tell, don’t ask. Por ejemplo, Service.price incluiría if/else o switch para decidir el precio del servicio, y así cualquier otro comportamiento asociado al tier.

En lugar de eso: polimorfismo.

Con polimorfismo crearíamos subclases de Service basadas en su tier: FreeService, PremiumService, ProfessionalService. Todas ellos responderían al mensaje price, cada una a su manera. La única decisión basada en el tier sería en el momento de la instanciación, posiblemente en una factoría. Gracias al polimorfismo, los consumidores no tienen que preocuparse de qué servicio concreto se trata. Los objetos derivados son más simples y fáciles de testear. Si en tu negocio se introduce un nuevo tier, no hay más que añadir una clase nueva y actualizar la factoría.

Por supuesto, el principio de sustitución de Liskov aplica aquí: subtipado semántico. También KISS, ya que los componentes serán más simples, manteniendo la complejidad bajo control y eliminando la necesidad de preguntar por el tipo.

Las clases base manejarán los mensajes comunes, y las variantes serán manejadas por los subtipos. Esto también podemos hacerlo con interfaces explícitas o implícitas.

La clave del polimorfismo está en identificar las variantes de comportamiento y expresarlas como especializaciones, cediendo el control a los propios objetos.

Programar sin ifs

31 Variaciones protegidas

Protected variations es un patrón GRASP y es una consecuencia de la aplicación de otros patrones y principios. En muchos sentidos se encuentra en la base de lo que es la orientación a objetos. Consiste en proteger a unos elementos de los cambios en otros.

Si hay algo que podemos dar por seguro es que las cosas cambiarán. El diseño de software tiene que tener en cuenta esto. Pero el objetivo no es prever cualquier cambio posible y tener una respuesta para cada uno de ellos. El objetivo es estar preparados para acomodar el cambio.

Pero, ¿quién debería hacerlo?

Veámoslo con un ejemplo. Supongamos un objeto Consumer que utiliza un objeto Service. ¿Quién debería proteger del cambio a quién?

Analicemos el caso de Consumer. ¿Debería estar preparado para los posibles cambios de Service?

No, dado que Consumer no tiene forma de predecir o anticipar los cambios de Service. No puede tomar medidas imaginando que esos cambios pueden producirse en un sentido u otro. ¿Entonces?

Entonces es Service quien tiene que proteger al resto del sistema de sus posibles cambios. ¿Cómo? Escondiéndolos en una interfaz estable, de modo que Consumer no necesite saber si Service ha tenido que cambiar o de qué modo.

Polymorphism es un ejemplo muy claro. Existe una ServiceInterface que puede ser implementada por diversas variantes de Service, de modo que Consumer no tiene que saber cuál está usando exactamente.

La posible fuente de cambio queda oculta para Consumer, de tal modo que podemos añadir variantes sin tener que tocar Consumer para nada.

Para ello el acoplamiento ha de ser el mínimo posible (inversión de dependencias, dependemos de la abstracción), Service estará cerrado a modificación (Open Close), las variaciones de Service serán intercambiables (Liskov), Consumer solo hablará con Service a través de su interfaz pública (Ley del mínimo conocimiento)

Hay una explicación muy completa aquí:

Protected variations

El riego de protected variations está en la posibilidad de sobre diseñar para prevenir cualquier cambio posible o si se identifican incorrectamente las posibles razones.

Pero básicamente se trata de que los objetos establezcan un contrato estable acerca de lo que hacen: qué mensajes les puedes enviar y qué respuesta o efecto puede producir.

32 Pura fabricación

El patrón GRASP que me quedaba por repasar es Pure Fabrication. Es como el patrón de último recurso, o el comodín de la llamada.

El problema que resuelve es el de no poder atribuir una responsabilidad a una clase concreta, fundamentalmente porque no tiene mucho sentido, o ninguno, desde un punto de vista semántico.

Esto puede ocurrir porque la clase necesita colaborar con otra. O bien porque la idea que se quiere expresar es, de alguna manera, artificial pero necesaria.

Por lo general, pretendemos que un diseño orientado a objetos exprese un modelo del mundo, o al menos de la parte del mundo en la que se desarrolla nuestro negocio. Por eso, sus conceptos, reglas y procedimientos, se representan con objetos.

Sin embargo, a veces necesitamos incluir alguna idea que no forma parte del negocio como tal, pero que nos ayuda a conseguir algo. Creo que un buen ejemplo es el patrón Repositorio.

Un repositorio no existe en el mundo real [^repo]. Necesitamos un repositorio para guardar entidades de dominio (pienso en el repositorio DDD) y reproducir su persistencia en el tiempo y su ciclo vital.

[^repo]; Bueno, puede existir, pero no es un concepto de negocio.

En DDD un repositorio busca proporcionar una ilusión de colección en memoria de las entidades, de modo que cuando necesitemos usar alguna tengamos algún lugar de donde obtenerla. Representaría la persistencia o continuidad de las entidades del mundo real.

Este concepto de repositorio, pues, es pure fabricación. Lo introducimos porque no podemos introducir la idea de persistencia en las entidades sin violar unos cuantos principios de diseño (active record, te estamos mirando a ti).

Algunos objetos que siguen el patrón Mediator entrarían también en esta idea de pure fabrication. Nos ayudan a evitar acoplamientos directos, introduciendo cierta artificialidad en el modelo. En DDD esto viene siendo un Servicio de Dominio.

Otro ejemplo que se me ocurre es un identity provider y cosas así. De nuevo, no forman parte del dominio modelado, pero nos ayudan a mantener un buen diseño.

Esto no debe ser excusa para introducir pure fabrication para resolver cualquier problema. Como decíamos al principio del capítulo es más bien un recurso cuando hemos agotado otras posibilidades. Voy a intentar poner un ejemplo.

En una aplicación bancaria supongamos que vamos a implementar un servicio de transferencia entre cuentas. Imagina que introducimos un TransferManager al que le pasamos las dos cuentas para ejecutar la transferencia.

Sin embargo, no tenemos necesidad, ya que podríamos tener métodos Account.makeTransfer(DestAccount) y Account.receiveTransfer(OriginAccount). No hace falta un Manager entre ambas.

De hecho, es posible que si tienes que nombrar a un servicio como Manager o similar, tengas un problema de diseño por exceso de fabrication. Así que conviene revisarlo.

En realidad, el exceso de Managers o Services puede derivar del hecho de no tener buenos fundamentos de OOP. Es un tema para otro momento y es el lamento de Alan Kay desde hace décadas.

Puedes ver una explicación sobre el asunto en estos vídeos (capítulos 10 y 11):

Object Oriented Design

Así que con este capítulo se acaban los patrones GRASP.

IV Diseño dirigido por dominio

El Diseño Dirigido por Dominio o DDD es una metodología propuesta por Eric Evans en su libro homónimo de 2004. Se trata básicamente de la aplicación de principios de diseño orientado a objeto para resolver problemas empresariales. Es casi un tópico decir que está destinado al diseño de sistemas complejos. A pesar de las advertencias, todo el mundo dice que está haciendo DDD, ¿verdad?

Después descubres que no es exactamente así, siendo indulgentes. Hay muchísimos proyectos que no necesitan todo el proceso de análisis y descubrimiento del dominio propuesto por DDD estratégico. Sin embargo, muchos equipos aplican muchos de los patrones tácticos descritos en el contexto de DDD y consideran que eso cualifica como tal.

En realidad, si no implica a toda la empresa lo más probable es que no estéis haciendo DDD. Y, oye, ningún problema con eso, pero no lo llames así. La mayor parte de las veces lo que estarás haciendo es desarrollar una arquitectura limpia en capas.

Con todo, los llamados patrones tácticos de DDD son fundamentalmente buenas prácticas de diseño orientado a objetos. Por esa razón, incluyo algunos en este libro.

33 Domain Driven Design

Extendámonos un rato sobre ese ente mítico del desarrollo de software: Domain Driven Design, o DDD para abreviar.

Con el DDD pasa como con otros muchos conceptos y paradigmas: hablamos sobre ellos de tercera o cuarta mano, o buscamos la receta para usarlo de alguna manera. Se dice que DDD es solo para desarrollos muy grandes y complejos. Sin embargo, mi opinión es que ofrece tantas ideas aplicables a cualquier escala que merece la pena el esfuerzo de estudiarlo aunque no utilices todo el instrumental estratégico si tu dominio es relativamente pequeño.

Pero ¿qué es DDD?

Fundamentalmente, DDD es una manera de afrontar el desarrollo de software complejo desde una perspectiva de Diseño Orientada a Objetos. Y entonces, ¿qué es complejo? Sinceramente, no tengo ni idea. Pero algo que he aprendido en estos años es que a poco que quieras tener impacto con tu negocio tienes que pensar en él como si fuese complejo, porque algún día lo será.

Básicamente, DDD nos propone una metodología que tiene dos grandes áreas: el DDD estratégico y el DDD táctico. El DDD táctico ofrece una serie de patrones que van orientados a la implementación. Ahora me quedo en el estratégico: DDD estratégico es una metodología para analizar y entender el dominio de nuestro negocio. ¿Y qué es el dominio? Pues aquello de lo que trata nuestra empresa.

Fíjate que no hablo de una aplicación, hablo de la empresa o del negocio. Así que el primer paso es entender el negocio mediante un proceso llamado knowledge crunching: obtener y procesar conocimiento de tu dominio.

Y esto, ¿cómo se hace? Pues: hablando con los expertos del dominio, o sea, las personas que trabajan cada día en tu empresa, con el objetivo de construir un modelo que represente su dominio.

Uno de los objetivos es desarrollar el lenguaje ubicuo. Es algo tan simple como utilizar las mismas palabras para designar los mismos conceptos en absolutamente todos los lugares, incluyendo el código, los tests o la documentación. Si a un cliente se le llama cliente, en el código se llamará cliente. Si a un proceso le llamamos Alta de Cliente, en el código se llamará Alta de Cliente.

Este lenguaje ubicuo se crea de manera interactiva. Desarrolladoras y Expertas de dominio hablan y exploran cada concepto o proceso, resolviendo ambigüedades y sobreentendidos, haciendo explícito lo implícito. Las desarrolladoras también pueden introducir términos en el lenguaje ubicuo porque, a veces, las expertas del dominio no son conscientes de algunos conceptos que manejan de forma implícita o que tienen sentido dentro de un contexto, pero no en otro. A veces, necesitas nuevos conceptos y metáforas que ayuden a darle sentido a todo.

Así que DDD tiene una buena parte de lingüística.

Otro de los objetivos del DDD estratégico es identificar los distintos subdominios o partes del dominio y cómo se relacionan.

¿Qué es lo que hace tu empresa? Eso es el dominio, pero para lograrlo tiene que hacer otras muchas cosas. Algunas son muy específicas de tu empresa, otras son comunes a todas las del sector, y otras son comunes a cualquier otra.

Eso que hace que tu empresa sea única, que las clientes la elijan, que nadie más sabe hacer como ella, es lo que llamamos el core domain o dominio principal. Si le quitas eso a la empresa, deja de existir. El core domain no lo puede hacer nadie por tu empresa, no se puede externalizar, no se debería hacer con aplicaciones de terceras partes. Nadie lo va a hacer como tú. Es donde tienes que poner la potencia de desarrollo.

Para poder realizar el core domain, la empresa necesita hacer otras cosas. Muchas de ellas son comunes a otras empresas del mismo sector, pero es posible que tú las hagas de manera especial para dar soporte a tu core. Por eso se llaman subdominio de soporte. Muchas veces podrás resolver esto con herramientas de terceros que te permitan suficiente personalización. A veces es algo que tendrás que desarrollar en casa. Posiblemente, no quieres externalizar esto.

Cada tipo de negocio tendría distintos subdominios de soporte. Quizá la atención al cliente puede estar ahí, al menos si te la tomas realmente en serio.

Después existen toda una serie de subdominios que son comunes a cualquier empresa, como nóminas o facturación, por señalar algunos ejemplos muy trillados. Son los subdominios genéricos, que puedes resolver externalizando o con herramientas de terceras partes.

Por cierto, que lo que para una empresa puede ser un dominio genérico o de soporte, para otra puede ser su core.

Este análisis te dice en qué te tienes que focalizar, dónde tienes que poner más esfuerzo y qué cosas debes resolver de la manera más sencilla posible externalizándolas o usando herramientas de terceros.

El análisis de dominio, en suma, consiste en definir el espacio del problema. Los subdominios tienen una representación en el espacio de la solución en forma de contextos acotados o bounded contexts.

Dentro de un bounded context viven conceptos y procesos propios, que no están en otras partes. Por otro lado, los conceptos generales del dominio pueden tener distinto significado según el contexto. Es decir, cada bounded context puede ver un concepto del dominio de forma diferente. Si vamos moviendo un concepto entre los distintos contextos podremos percibir esos cambios. Una cliente no es lo mismo desde el punto de vista de facturación, de ventas o de marketing. Pero mantiene una identidad a través de todos ellos.

Por otro lado, los contextos mantienen relaciones entre ellos. Algunos son más próximos, otros se superponen en cierta medida, otros tienen una relación de dependencia. Este análisis busca obtener un context map o mapa de contextos, que nos dice cómo se regula la relación. Todo esto merece un estudio detallado. También merecen mención los llamados building blocks. El dominio se expresa en código usando una serie de elementos: entidades, value objects, servicios y agregados.

Así que, resumiendo mucho, DDD es una manera de abordar el desarrollo de software en entornos empresariales complejos. Incluye un análisis estratégico en el que se busca desarrollar un modelo del dominio que luego se representará mediante software.

34 Entidades

Toda aplicación, por pequeña que sea, tiene que contener un modelo del mundo. Al menos de la parte del mundo que es su dominio. En DDD esto se hace de forma intencionada y explícita, pero con frecuencia, usamos metodologías menos rigurosas y el modelo queda difuso.

Incluso el típico primer programa para sumar dos números contiene un modelo de ese dominio en el que representamos conceptos como sumandos, operación y resultado. Solo que solemos mezclarlo con la presentación y otros.

Por ejemplo: print (a + b) a y b son los sumandos, + representa la suma y (a+b) el resultado. He aquí un modelo de un dominio muy pequeñito y limitado. Pero es un modelo de ese trocito del mundo.

Claro que nosotras solemos trabajar modelando dominios bastante más complicados que ese, que implican diversos conceptos e interacciones entre ellos. Incluso aunque no hagas DDD, ser consciente de esto y expresarlo en código de forma separada es una gran inversión.

Como ya hemos comentado, en DDD la capa de dominio es una representación en código del modelo que no tiene más dependencias que el propio lenguaje de programación. Es decir, el dominio se expresa mediante objetos puros del lenguaje en que se desarrolla1.

En este punto se introducen los llamados building blocks: entidades, value objects, servicios y agregados. ¿Cabría considerar aquí los eventos de dominio?… bueno, ya veremos.

Estos building blocks también se podrían utilizar aunque no hagas realmente DDD, es decir, si quieres tener una capa de dominio con un modelo rico.

Antes de continuar, una nota superimportante: la capa de dominio es agnóstica acerca de la tecnología de persistencia. Es decir, la capa de dominio no sabe nada acerca de bases de datos o whatever sistema de persistencia. Pero me estoy adelantando.

En este capítulo quería hablar de las Entidades

Las entidades son la representación de conceptos del dominio que tienen:

  • identidad: cada instancia es diferente aunque tenga las mismas propiedades.
  • ciclo de vida: el estado cambia a lo largo de ese ciclo.
  • comportamiento: hacen cosas del dominio.

Estas entidades nos interesan por su identidad.

Identidad es eso tan difícil de definir en la vida real, pero que sin embargo percibimos en nosotras mismas, en las demás personas, en los animales, en los objetos. Esa identidad persiste a pesar de los cambios que suceden en el tiempo.

Siempre es el mismo río, pero con distinta agua.

Esa identidad la podemos representar con cierta facilidad mediante una propiedad específica, un identificador que es único en el contexto de la aplicación. Aunque puede ser único en un contexto más amplio usando identificadores universales UUID2.

La identidad de las entidades representa la identidad de los objetos del mundo real. Nos permite trabajar con un ejemplo concreto, por eso la necesitamos.

La identidad persiste en el tiempo… he usado la palabra persiste con intención. En un mundo ideal, las entidades estarían en la memoria del sistema durante todo su ciclo de vida. Pero en el mundo real los ordenadores se paran, se reinician.

Necesitamos una forma de persistir la identidad en el tiempo. DDD introduce el concepto de repositorio para lograr esto y, ojo a la definición: un repositorio proporciona la ilusión de una colección en memoria en la que se pueden guardar o recuperar entidades.

Una ilusión de colección en memoria.

No un acceso a una base de datos. Esto es superimportante entenderlo bien. Los repositorios no son la puerta de acceso a la base de datos. Puedes implementarlos con una tecnología concreta de base de datos, pero no es el acceso a ella.

Si el dominio necesita crear una entidad, lo hace y mientras no la usa, la pone en el repositorio y la recupera por su identidad cuando sea necesario. El cómo se las arregla el repositorio para hacer eso no le interesa al dominio para nada.

Si la entidad contiene dentro colecciones del otras entidades hijas (por ejemplo Pedido/Items) es problema de la implementación del repositorio lidiar con los detalles de cómo guardarlas o recuperarlas del almacenamiento físico. Pero el dominio solo ve que pone allí Pedidos y puede recuperar Pedidos por su id. Y quien dice Entidades, dice Agregados, pero esa es otra historia.

Las entidades tienen ciclo de vida, esto es: se crean, hacen cosas y les pasan cosas y pueden llegar a morir de algún modo. Por ejemplo, un pedido se inicia, se le añaden productos, se prepara, se envía, se factura y, una vez entregado y todo en orden, acaba su proceso.

También tienen comportamiento. Todos esos pasos del proceso cambian el estado de la entidad, pero es cosa de la entidad mantener su estructura interna, cumplir la reglas de negocio y las invariables. Por ejemplo, el pedido puede añadir productos como items del pedido.

O puede ser facturado, enviado, etc, etc. Todo esto se refleja en sus comportamientos: Order.start, Order.addProduct, Order.invoice, Order.cancel… En cada uno de estos comportamientos, Order tiene que asegurar que se cumplen las reglas de negocio.

Por ejemplo: solo se pueden añadir 3 productos del mismo tipo. Pues de eso se encarga Order en addProduct. Que solo se puede facturar un pedido si tiene productos, que no se puede cancelar un pedido facturado, etc. De todo eso, al ser reglas de negocio se encarga Order. Para eso es el Information Expert.

Podríamos decir que Order es aquí un agregado, pero me gustaría dedicar un capítulo a eso específicamente. Pero sí, sería un agregado.

Así que ahí tenemos a las entidades, representando conceptos de nuestro dominio con identidad. Como veremos las entidades pueden incluir Value Objects. Además, las entidades generan Eventos de Dominio. Ya tendrán su capítulo también.

¿Que cómo persistimos entidades en un repositorio o cómo las buscamos? Lo hablamos en otro capítulo, porque hacerlo realmente bien, sin acoplarse a la tecnología de persistencia completa da cierto trabajo, pero diría que merece la pena.

35 Value Objects

¿Qué pasa cuando los conceptos del dominio no nos interesan por su identidad, sino por su valor? Pues que tenemos un nuevo building block del DDD: los Value Objects.

¿Qué significa que un concepto nos interesa por su valor? Pues básicamente que ese concepto tiene una o más propiedades y los diferenciamos porque sus valores son diferentes, o los consideramos el mismo si sus valores son iguales. Pensemos por ejemplo en una longitud.

Una longitud de 5 metros es igual a otra longitud de 5 metros. Parece obvio, pero eso quiere decir que podríamos usar la misma instancia de un objeto longitud para representar esa propiedad incluso en diferentes entidades. Pero bueno, eso es una sutileza que igual no nos ayuda.

Por comparar, dos entidades que tengan exactamente las mismas propiedades excepto la identidad serían distintas y se representarían con instancias distintas.

Aunque digamos que los Value Objects nos interesan por su valor no son simples contenedores de datos. Eso serían los DTO, por ejemplo y cierta Java old school llamaba value objects a lo que no eran otra cosa que DTO. Los Value Objects en DDD tienen comportamiento también.

Y es un elemento superimportante. Mathias Verraes decía en un post en su blog que los VO atraen comportamiento.

Pero, ¿cómo nace un VO? Imagina que tienes que modelar el concepto Precio. El Precio puede representarse con un float. Dos consideraciones:

  • No todos los float pueden ser precios, pues los precios siempre son positivos.
  • La representación no es completa sin la unidad monetaria. Así que un precio necesitaría dos variables.

O sea que en realidad tenemos un float limitado y un string. Así que los juntamos en un objeto que tendrá dos propiedades, Por ejemplo: amount y currency, y unas reglas de construcción, amount es mayor o igual a 0 y puede tener decimales, currency es un string representando una moneda según la norma ISO 4217; y con eso tendremos un VO Price.

De hecho, Currency también es candidato a VO porque aunque es un string, no todas las infinitas string posibles representan un valor adecuado para currency. Así que Currency puede ser un VO que tiene un valor string el cual solo puede ser uno de EUR, USD, YEN, GBP, etc…

En cierto modo, los VO nos sirven para extender el sistema de tipos con aquellos propios de nuestro dominio. Veamos ahora algunas propiedades interesantes que deben tener los VO.

Igualdad por valor: como hemos dicho, dos VO son iguales si tienen los mismos valores. Normalmente, tendremos que introducir un método equals (que recibe otro VO del mismo tipo) que nos permite chequear esa igual conforme a las reglas relevantes en cada caso. A veces no podemos comparar todas las propiedades del VO o tenemos que tener en cuenta más cosas en la comparación. Por ejemplo, no puedes decir que 10 USD = 10 EUR, tienes que convertirlos primero.

Inmutabilidad: Los VO representan un valor, simple o compuesto. Si ese valor cambia, se debe cambiar la instancia por otra con el nuevo valor. Si cambia la propiedad precio de un producto, no mutamos el objeto Price, sino que reemplazamos la propiedad con una instancia distinta de Price.

De este modo, si el objeto Price tiene métodos mutator, por ejemplo: para subir o bajar el precio un porcentaje, en realidad serán métodos factoría que nos devuelven una nueva instancia con el valor resultado de la operación.

Esta inmutabilidad es la que nos permite usar la misma instancia en diferentes lugares.

Antes mencioné que el VO Price tenía unas propiedades y unas reglas. Los VO se han de crear válidos y consistentes conforme a las reglas que los definen. Por ejemplo, unas coordenadas tienen que estar en un rango de valores (latitud 0 a ± 90, longitud 0 a ± 180), y tienen que ser dos valores. De este modo, si tenemos un VO instanciado ya sabemos que es válido y consistente y lo podemos usar sin más.

Podemos modelar como VO cualquier concepto de dominio que no tenga identidad y nos interese su valor. Habitualmente serán conceptos relacionados con la cuantificación o la medida, pero diría que cualquier concepto que requiera algún tipo de regla de dominio o negocio es modelable como VO. Como reglas prácticas:

  • Un concepto que se puede modelar con un tipo básico, pero que requiere alguna regla de validación.
  • Un concepto que requiere más de una variable para su representación.

Decimos que los VO atraen comportamiento. Entre otras razones, es porque también son responsables de mantener sus propias invariantes y, aplicando Tell, don’t ask, las entidades no deben preguntar al VO por su estado para hacer algo, sino que más bien delegan en el VO todo lo que tenga que ver con el VO. Puedes ver un ejemplo en este artículo (al hablar de TaskStatus)

Preview

El camino a DDD

Por cierto, que refactorizar a VO es un muy buen primer paso para mejorar un código existente. Y dentro de los VO me gustaría señalar también las virtudes de los Enumerables, que serían un tipo de VO también.

En fin, podría hablar de VO y ejemplos de modelado con VO de aquí a final de año, así que lo dejo por ahora. En este artículo tienes un buen resumen:

Value Objects

36 Agregados

Los agregados en DDD… Quizá sería mejor hablar de Aggregate Roots, o raíces del agregado. Pero bueno, ¿qué es esto y por qué tengo que saber sobre ellos?

Es fácil ver que los conceptos tienen muchas maneras de relacionarse. Unos conceptos pueden contener otros, referenciarlos o poseerlos. Así, por ejemplo, una factura se refiere a una cliente, contiene un importe, una forma de pago y tiene líneas en las que se detallan los conceptos y precios, entre otras cosas.

Todos ellos viven en la factura, aunque de distinta forma. La cliente lo hace de manera independiente, así que la factura solo necesita tener una referencia que las asocie.

Sin embargo, las líneas no son independientes. Viven literalmente dentro de la factura. No existe la idea de línea de factura fuera de ahí. Solo podemos llegar a ellas a través de la factura.

Este conjunto de conceptos relacionados que van juntos y son tratados como una unidad sería un agregado, aunque habitualmente nos referimos a él por su punto de entrada o raíz del agregado o aggregate root.

Pero hay más. Domain Driven Design tiene mucho que ver con los límites. Cada unidad en DDD (y en OOP si vamos a eso) se encarga de definir unos límites. Por ejemplo, una clase (entidad o value object) define límites de consistencia para los objetos: se crean de forma que sean consistentes, válidos, con todo lo necesario.

Los agregados definen límites de consistencia para el conjunto de entidades que agrupan. Protegen activamente sus invariantes que, en este contexto, son las reglas que rigen sus relaciones internas. Y es la raíz del agregado la que se encarga de ello.

Aplicando el patrón Creator de GRASP, el agregado es responsable de crear o instanciar sus entidades hijas, de modo que para añadir líneas a una factura no instancias líneas y las pasas al agregado. En su lugar, pasas los datos necesarios al agregado para que las instancie.

De ese modo, no hay líneas pululando por la aplicación que estén fuera de un agregado. El resto de la aplicación no sabe nada de ellas.

Esto también define límites de transaccionalidad al llevar esto a la persistencia. El agregado se persiste como una unidad y es el repositorio del agregado el responsable de guardar la información como sea más conveniente. En el caso del ejemplo de las facturas, no existirían repositorios para las líneas de factura. En su lugar, la implementación en base de datos relacional del repositorio de facturas hablaría con varias tablas.

En consecuencia, los repositorios reciben y entregan agregados. Un reverso de esto es que solo se persiste un agregado por transacción.

Algunas cuestiones:

Los agregados son conceptos bastante fluidos y lo que es raíz del agregado en un contexto puede no serlo en otro. Ejemplo: una cliente puede ser raíz del agregado y contener todas sus facturas. Pero en otro contexto, la raíz del agregado es la factura y contiene una referencia a cliente.

Esencialmente, los agregados son equivalentes a las entidades. Tienen identidad, tienen ciclo de vida. La diferencia es que sus invariantes, o reglas de negocio, regulan la relación entre varios objetos, mientras que las entidades son objetos discretos, que se encargan de proteger sus invariantes internas.

¿Un bounded context debe tener solo un agregado? Nope. Un agregado representa un concepto complejo del negocio que está compuesto por varios conceptos relacionados. En un contexto puede haber un único agregado, en otros más de uno. La función de los agregados es representar esos conceptos complejos ofreciendo un único punto de acceso y garantizando su consistencia coordinando esos objetos agregados.

Es decir: cada objeto es responsable de su propia consistencia, y la raíz de un agregado es responsable de la consistencia del conjunto agregado.

Aquí una serie de artículos bastante completa acerca de los agregados:

(images/domain-driven-design-aggregates.png)

Personalmente, diseñaría los agregados de arriba hacia abajo. Es decir, considerando primero los conceptos más generales, que podrían acabar siendo raíces de agregado, y bajando luego a los detalles, guiándome por lo que dicen las reglas de negocio que apliquen.

Acerca de la persistencia de los agregados… En este artículo de Matthias Noback se dice que a la hora de diseñar agregados te olvides de la persistencia.

Doctrine ORM and DDD aggregates

Lo dice en el sentido de no dejar que las cuestiones de persistencia, que son de infraestructura, condicionen tu diseño del agregado, que es del dominio. De hecho, un poco más abajo propone otra regla: actúa como si fuesen a guardarse de una base de datos orientada a documentos. Un noSQL, cosa que encaja realmente bien con la misma idea de agregado. Para entender mejor esto hay que repetir la idea de que un repositorio no es la puerta de acceso a la base de datos y añadir que la estructura de tablas en una base de datos relacional no tiene que reflejar la estructura del agregado.

Me dan ganas de repetir esto: si el sistema de persistencia es una base de datos SQL, la estructura de tablas en la base de datos relacional no tiene que reflejar la estructura del agregado.

Esto quiere decir que a la hora de persistir lo que guardamos es una representación del agregado, no el agregado como tal. Para eso nada mejor que una base de datos orientada a documentos. Por ejemplo, en un caso de agregado con más de media docena de entidades podrías necesitar tan solo un par de tablas para persistirlo.

Eso puede suponer algo de des-normalización, lo que seguramente hará más sencillo todo con la base de datos y hasta me atrevería a decir que podría ser bueno en lo que toca a performance.

De hecho, el objetivo sería que el agregado y su persistencia pudiesen evolucionar independientemente. Dentro de lo que cabe, por supuesto. Seguramente los Database Administrator del mundo me quieran dar collejas, pero DDD es code driven y es la base de datos la que tiene que adaptarse.

Según E. Evans, desde el punto de vista del dominio, un repositorio es una colección en memoria de agregados, que nosotros tenemos que implementar con algún mecanismo de persistencia no volátil.

Eso sí, tener esos datos en una base de datos nos permite crear servicios que puedan acceder a ellos para otras necesidades de la aplicación, como obtener listados para vistas, sin necesidad de pasar por los repositorios.

37 Servicios de dominio

Otro de los building blocks de DDD son los Servicios. Ya me estoy esperando la pregunta:

– Pero, ¿Dónde van los servicios?

Así que vayamos por partes, que primero tenemos que entender lo que son los servicios.

En DDD los procesos del negocio se representan preferentemente como comportamientos de entidades (o agregados) de la capa de dominio. Es decir, si es posible atribuir todo el proceso a una entidad (o agregado).

Ejemplo, aplicación bancaria: un traspaso entre cuentas puede modelarse en la entidad Account, pasándole la otra cuenta y el importe del traspaso. Lo que sea necesario saber está en Account, tanto si actúa de emisora como si es receptora.

Ahora bien, supongamos que el proceso requiere información que no está disponible en la entidad (o agregado), especialmente si el acceso a esa información requiere algún tipo de dependencia externa. Se necesita algún proveedor de esa información (o validador o whatever).

Por ejemplo, la transferencia entre cuentas de distintos bancos seguramente requiere algún tipo de servicio.

O bien, no hay una relación entre las entidades (o agregados) que participan en ese proceso. Dicho de otra forma: necesitamos que sean independientes aunque puedan intercambiar información…

¿Te suena el patrón Mediator o Indirection?

Pues eso es básicamente un servicio: un objeto mediador que provee un comportamiento que no puede ser proporcionado completamente por una entidad (o agregado), que puede requerir colaboración de otras entidades (o agregados) de forma que no se cree una dependencia entre ellas.

Los servicios de dominio representan procesos del negocio que implican o la coordinación de entidades (o agregados) o requieren una dependencia fuera del dominio que se debe invertir. Un ejemplo de servicio de dominio es… ¡tachán! El repositorio.

Una llamada a una API de terceros para obtener información (imagina un convertidor de moneda) también se modelaría como servicio de dominio usando inversión de dependencias. En pocas palabras:

Si modela un proceso de negocio es un servicio de dominio. Si no tiene dependencias externas al dominio se implementa como un objeto de dominio más, si las tiene, se invierten las dependencias as usual.

¿Servicios de aplicación? ¿Es que nadie piensa en los servicios de aplicación? Ya hablaré de estos en otro momento si me da el cerebro, pero hoy no es ese día.

Solo decir que la ubicación de un servicio en la capa de dominio o de aplicación depende fundamentalmente de si modela o no un proceso del negocio o dominio.

Un ejemplo típico es de las notificaciones por email… Eso es de aplicación, que enviar emails no es de dominio…

A ver. La RGE (Respuesta Gallega Estándar) aplica aquí:

– Depende.

¿Es notificación un concepto importante del dominio? La vía de notificación, ¿es relevante de algún modo? Es muy posible que tengas que modelar un servicio de dominio de notificación que pueda atender a las preferencias de la cliente en cuando a la forma de la misma.

De hecho, el mecanismo (email, WhatsApp, Slack, whatever) es una cuestión de la capa de infraestructura. Lo que suele ocurrir es que la notificación se gestiona habitualmente en el caso de uso (capa aplicación) bien sea directamente o mediante un suscriptor al evento interesante.

Esto puede llevar a confusión. Pero del mismo modo usamos repositorios en la capa de aplicación porque los casos de uso son coordinadores de objetos de dominio (otro mediador) para dar cumplimiento a una intención de la usuaria.

Por otro lado, es cierto que hay servicios de notificación que podrían vivir únicamente en la capa de aplicación, pero precisamente porque solo están interesados en aspectos técnicos del funcionamiento de la aplicación. Ejemplo: logs y similares.

En resumen, si me preguntas, te diría que es muy probable que ese servicio que estás buscando ubicar pertenezca al dominio si modela algún proceso del negocio.

Una regla práctica para identificar un servicio de aplicación sería si podemos trasladar el mismo servicio a otro proyecto. Es decir: es de dominio si solo tiene sentido en nuestro dominio, y es de aplicación si se podría usar en cualquier dominio.

Con todo, no hay reglas mágicas. DDD no es un conjunto de recetas para organizar una aplicación, sino un paquete de herramientas para poder entender y modelar un dominio.

38 Eventos

Tenía pendiente hablar de eventos. No de saraos, claro, sino de mensajes.

Hace tiempo comenté que había tres tipos de mensajes:

  • imperativos (commands): producen un cambio en el sistema
  • interrogativos (queries): recuperan info del sistema
  • enunciativos (events): informan de algo que ha pasado en el sistema

Los tres mensajes se pueden modelar con un patrón message + handler. Message es un objeto inmutable que contiene la información interesante y Handler es un objeto que recibe Message como parámetro y hace lo que tenga que hacer.

Tanto los command como las queries tienen un único handler o destinatario (1:1). Sin embargo, los eventos pueden tener un número indefinido de destinatarios (0:n).

Los handlers de commands y eventos no devuelven nada pues son comandos. Y en el caso de los eventos… es que no hay nadie para escuchar lo que respondan.

Los eventos son clave en las arquitecturas limpias dado que nos permiten algunas cosas importantes, entre ellas:

  • Que los casos de uso sean single responsibility.
  • Que partes separadas de la aplicación puedan comunicarse sin acoplarse.
  • Que puedas añadir funcionalidad a una aplicación sin modificar el código existente, tan solo añadiendo nuevos subscriptores de los eventos apropiados.

¿Cómo funciona esto de los eventos? Básicamente se hace a través de un patrón de suscripción. Un mediador (EventDispatcher) puede recibir eventos y despacharlos a todos los handlers que se han apuntado como suscriptores.

Así, cuando ocurre algo interesante se instancia el evento correspondiente y se pasa al EventDispatcher. Este identifica los posibles handlers y les pasa el evento, ejecutando el handler.

Este proceso así explicado es síncrono. Cuando despachamos el evento la ejecución pasa al primer handler suscriptor, luego al siguiente, y así hasta acabar la lista, y luego vuelve al punto donde estábamos.

En asíncrono la ejecución de los diferentes handlers se hace, teóricamente, en paralelo. Conceptualmente, es la misma idea: al lanzar el evento se ejecutan ciertas acciones en respuesta.

By the way, creo que es buena idea considerar los eventos como si fuesen asíncronos, aunque no lo sean, y todo lo que eso conlleva. Es decir, un evento no puede depender de lo que haga otro antes o después.

Algunas consideraciones: En aplicaciones que la separan, la capa de dominio es en la que se producen los eventos. ¿Quiénes generan eventos? Pues normalmente, entidades y agregados. Les pasan cosas interesantes y lo dicen.

Ahora bien, hay que tener cuidado con temas como los límites transaccionales. Por ejemplo, para emitir el evento de que un agregado ha sido creado, tenemos que estar seguros de que el agregado está persistido. No bastaría con haberlo instanciado.

Por eso, aunque en teoría el evento se emite cuando se produce, lo normal es que entidades y agregados instancien y guarden los eventos y que estos se despachen en el caso de uso, una vez que sabemos que todo se ha hecho bien. Así, por ejemplo, hemos creado el agregado y lo hemos pasado al repositorio, que no se queja. Por tanto, podemos asumir que el agregado estará disponible.

Consejo: no uses herencia para tener gestión de eventos en agregados, extendiendo de una clase base AggregateRoot. En su lugar emplea traits o composición.

¿Dónde viven los handlers de los eventos? Pues mi preferencia personal es ponerlos en la capa de aplicación como si fuesen casos de uso. Los eventos en sí en la capa de dominio. En cierto modo, es como si EventDispatcher fuese un Controller.

Decía que los eventos nos proporcionan SRP para los casos de uso. Así es: tendremos un caso de uso que realiza la acción principal, emitiendo uno o más eventos, y los suscriptores nos permiten añadir acciones (notificación, proyecciones o lo que sea) sin contaminar el caso de uso.

Igualmente, nos ayuda con Open/Close, ya que no tenemos que tocar el caso de uso si tenemos que añadir funcionalidades o acciones. Escribimos un nuevo suscriptor y ya.

Por otro lado, es igualmente fácil quitar suscriptores o reemplazar por uno nuevo.

No he entrado aquí en un tema más complejo como es el event sourcing. Es algo que se escapa del scope del capítulo, pero básicamente consiste en no mantener estado en los agregados, sino los eventos que conducen a ese estado.

39 Sobre la persistencia en DDD

Hay un temilla relacionado con DDD sobre el que me han preguntado varias veces, aunque no es exclusivo de DDD, sino de cualquier tipo de arquitectura limpia, incluyendo la Hexagonal… (música ominosa…) LA PERSISTENCIA.

O incluso diría que más que la persistencia, el tema es: cómo tratar la relación entre Entidades de dominio y bases de datos. Si sí, si no o si todo lo contrario.

Porque cómo va a ser que una aplicación no tenga base de datos, dónde va a parar. Que una aplicación sin base de datos no es nada, es una mindundi de las aplicaciones, el nivel más bajo del escalafón.

Pues todo se resume en una frase sencilla: la base de datos es un detalle de implementación.

Y ya.

Veamos. La cuestión clave es que DDD define la capa de dominio como agnóstica del mecanismo de persistencia. En otro capítulo mencioné que un repositorio no es más que un espacio en memoria en donde guardamos las entidades/agregados para cuando necesitemos usarlas.

Y, de hecho, un repositorio no tiene más métodos que los necesarios para guardar o recuperar entidades individuales o grupos de entidades que cumplan un criterio.

Desde el punto de vista del dominio eso es lo único que sabemos. El cómo se las arregla el repositorio para hacerlo es SU problema y si guarda las cosas en la RAM, en un disco o en configuraciones cuánticas entrelazadas es SU problema, no problema del dominio.

En esencia, lo que se pretende decir es que el dominio se debe modelar sin pensar en que la información se persistirá con una tecnología concreta. De hecho, entidades y agregados exponen comportamientos que podemos invocar enviándoles mensajes. No sabemos nada de sus propiedades.

Las propiedades de entidades, de agregados o de cualquier objeto son SU problema. Orientación a objetos: information hiding, de los information hiding de toda la vida.

El problema viene porque, por lo general, los mecanismos de almacenamiento requieren acceder a la estructura interna de las entidades/agregados, acceder a sus propiedades de alguna manera. Incluso bases de datos orientadas a objetos tienen que serializarlos en forma de documentos o algo.

En fins. Veámoslo ahora desde otro prisma. Con frecuencia usamos alguna librería o framework que nos ofrece un patrón para lidiar con una tecnología de base de datos. Lo típico sería una base de datos relacional con SQL. Así que se han inventado patrones como Active Record, librerías de ORM, que gestionan por nosotras las transformaciones entre objetos del lenguaje y su representación en un sistema de base de datos. Y aquí es donde empieza el lío porque estas librerías nos van a ofrecer extender sus modelos para crear los nuestros, como active record.

O bien nos van a dar ciertos requisitos para crear nuestras entidades de modo que sean persistibles, o que se puedan mapear de alguna manera. Verbigracia, con annotations o mapeos.

Esto presenta algunos problemas bastante gordos. Con Active Record tenemos una violación del SRP: toda entidad tendrá dos responsabilidades/razones para cambiar: la suya propia y las derivadas de saber persistirse esto. Sucede así porque en Active Record un objeto es como un proxy a una fila de una tabla de una base de datos y a sus relacionadas. Si hay que cambiar algo para la persistencia la entidad tendrá que cambiar. Aparte seguramente no podrás testear estas entidades aisladamente y necesitarás: ¡ta-chan!, una base de datos activa para poder hacer un test. Esto pinta bastante mal.

Y con otros patrones la cosa mejora más o menos, porque tus entidades de dominio pueden verse contaminadas por necesidades del ORM, como tener que exponer getters/setters o propiedades públicas. No queremos eso en nuestras entidades de dominio, ¿verdad?… ¿Verdad?

Esto es un follón de narices porque finalmente tenemos que aceptar algún tipo de compromiso. Matthias Noback propone algunas soluciones aquí

DDD and your database

Creo que la raíz de las dificultades está en que normalmente consideramos la base de datos como algo nuestro e inherente a la aplicación y nos cuesta mucho entender que lo que guardamos en la base de datos no es otra cosa que una representación de nuestros objetos de dominio.

Por otro lado, esta representación no tiene que ser isomórfica a las entidades, pero sí contener información suficiente como para reconstruirlas. Es decir: no tienes que tener los mismos campos, no todas las entidades en el dominio tienen que tener su propia tabla en la base de datos…

Toda la clave está en cómo una implementación del repositorio se las arregla para extraer la representación adecuada para guardarla en una base de datos. Si esa representación usa el patrón Active Record u otro es indiferente porque solo afecta al ámbito del repositorio y no al dominio.

Existen patrones que pueden usarse para hacerlo. Por ejemplo, esta sería una idea.

Representation pattern

Aunque el ejemplo está orientado a presentación, también aplicaría para persistencia. Se trata de una forma de obtener una representación de una entidad sin dependencia.

Por otro lado, nos queda el asuntillo de acceder a información que está en base de datos sin tener que traerme agregados enteros.

Esto tiene que ver con los ViewModel. ¿Qué pasa si tengo una vista que solo necesita unos pocos campos de una entidad? ¿Añado métodos al repositorio para esto?

No.

Necesitas crear otro tipo de servicios que puedan implementar acceso a la base de datos de forma que exponen métodos que nos permiten obtener ViewModels. Los ViewModel son poco más que DTO que usamos únicamente para poder poblar esas vistas con información.

Básicamente, se necesita un ViewModel por vista. La implementación del ViewModelRepository si lo quieres llamar así es muy sencilla: es básicamente una query SQL. Además, es muy fácil añadir soporte para filtros o criterios de ordenación, que son concerns de las vistas.

El caso es que con esto estamos aplicando un patrón MVC (o MVVC) el cual no tiene que pasar para nada por el dominio. Los ViewModelRepository puedes invertirlos, definiendo interfaces en la capa de aplicación, que se implementan en la infraestructura.

Oye, que en mi caso no es una vista, que necesito alguna información para que el dominio haga algo, pero está en base de datos, o en un yaml o algo así ¿Qué hago? ¿Tiro de repositorio?

No.

Creas abstracciones en el dominio de esos servicios con los que obtener tal información, implementando en infraestructura con la tecnología de persistencia de que se trate.

En resumen: todo lo que sea acceder a algo que está fuera de la aplicación (como una base de datos) y que en último término no es otra cosa que un estado global se debe abstraer.

Lo dicho, que la base de datos no es más que un detalle de implementación.

40 Domain Driven Design y Arquitectura Hexagonal

Es muy común identificar, erróneamente, DDD con la Arquitectura Hexagonal de Alistair Cockburn. Como si fuesen la misma cosa o estuviesen ligadas de alguna manera. A veces le llamamos a eso directory driven development. Eric Evans cita la arquitectura hexagonal como una implementación de un bounded context compatible con DDD.

¿Y por qué esta identificación? Pues en parte porque hay coincidencias entre Arquitectura Hexagonal y las capas que propone Evans (dominio, aplicación, infraestructura, UI), aunque a decir verdad la Arquitectura Hexagonal no postula capas, sino una separación entre la aplicación y los detalles técnicos de implementación. La Arquitectura Hexagonal tiene un objetivo que las implementaciones de DDD también deben cumplir:

Aislar el dominio.

En DDD la capa de dominio es donde reside nuestro modelo. El modelo estará escrito en objetos puros del lenguaje, sin más dependencia técnica que el propio lenguaje de programación. El dominio no debe saber nada de los detalles de implementación. Si algo necesita una tecnología concreta lo correcto es definir una abstracción en la capa de dominio (dependency inversion) de modo que el detalle de implementación dependa de la abstracción. El ejemplo típico es el patrón repositorio.

Pues esto mismo es lo que nos dice la arquitectura hexagonal. La aplicación, que coincide más o menos con el dominio en DDD, no debe tener contacto directo con el mundo exterior, sino exponer Puertos (básicamente abstracciones -> interfaces) de forma que las dependencias o cualquier comunicación con el mundo exterior (UI, API, whatever) sea representada en el sistema por un Adaptador que cumple esa interfaz o usa los puertos que se exponen. Puertos/Adaptadores es el segundo nombre de la Arquitectura Hexagonal.

De hecho, el concepto de Puerto es algo más amplio que la mera aplicación de la inyección de dependencias. Los puertos se definen más bien por su rol, administración, interfaz de usuario, sincronización, persistencia, que por una tecnología concreta.

De hecho, los tests interactúan con la aplicación mediante esos puertos. Una aplicación hexagonal es testeable por diseño.

Así que la Arquitectura Hexagonal aísla y protege al dominio del mundo exterior, con lo cual es una buena forma de estructurar una aplicación sin usar DDD, y también para implementar un bounded context en DDD.

Si bien, como hemos dicho hay tamaños de dominio para los que DDD es excesivo, la arquitectura hexagonal puede ser una buena forma de empezar siendo pequeños. Mucho mejor que FDD (Framework Driven Development), donde va a parar.

¿Por qué? Porque FDD te acopla desde el inicio a un detalle de implementación, mientras que Arquitectura Hexagonal te obliga a mantener el framework en su lugar, dejando el dominio aislado. Es un poco más de esfuerzo, pero el día que el negocio se empiece a complicar por temas de nuevas prestaciones, escalado, etc., ¡Ay! ¡Cómo lo vas a agradecer ese día! E incluso si algún día se complica lo suficiente como para que DDD tenga sentido, estarás en mejor posición.

Pero también hay que decir lo mismo de las arquitecturas limpias en general. ¿Y qué es una arquitectura limpia?

Para mí una arquitectura limpia cumple tres criterios muy sencillos:

  • Capas: hay una separación de intereses en capas
  • Regla de dependencia: las dependencias apuntan en una sola dirección
  • Inversión de control: los detalles dependen de abstracciones

Una arquitectura contiene tres grandes grupos de elementos:

  • Un modelo del mundo: la representación del dominio
  • Una representación de las metas o intenciones de las consumidoras de la aplicación: los casos de uso
  • Las implementaciones técnicas que la hacen funcionar

Que coinciden con las capas de:

  • Dominio: el modelo del mundo
  • Aplicación: los casos de uso
  • Infraestructura: las implementaciones, que no tienen dependencias cruzadas

La Arquitectura Hexagonal me permite tener todo esto. Algo que funciona bien tanto para una aplicación pequeña, como para un bounded context en DDD, porque cada bounded context puede tener su propia arquitectura.

Así que si bien es completamente incorrecto decir que Arquitectura Hexagonal es DDD, sí que es una opción muy válida para su implementación.

Y Arquitectura Hexagonal no es la única cosa que se identifica erróneamente con DDD. Ojo.

41 Las vistas en DDD

Tienes un caso de uso: mostrar todos los pedidos que han caducado. Así que en el dominio de pedidos, defines OrderRepositoryInterface, y agregas getExpiredOrders, pero…

Para explicar una solución rigurosa a este problema tendría que haber hablado de eventos antes, por lo que haré un inciso con unos pocos párrafos al respecto y luego me meto en harina.

En una aplicación pueden circular tres tipos de mensajes. Dos de ellos los conocemos ya: imperativos (comandos) e interrogativos (queries).

El tercer tipo son los mensajes enunciativos: los eventos. Los eventos comunican cosas interesantes que han ocurrido en el dominio.

Comandos y Queries tienen un solo destinatario. Los eventos pueden ser escuchados por cualquier número de interesados, que son sus Listeners o Subscribers. Estos se apuntan a responder a ciertos eventos y actúan cuando estos son lanzados por un despachador de eventos.

Los eventos los generan las entidades y/o agregados. La forma más habitual de gestionar esto es que en lugar de lanzar los eventos exactamente cuando se producen, se acumulan y se recolectan para lanzar una vez que los casos de uso hayan terminado con éxito.

Y hasta aquí esta breve introducción. Quédate con que si se produce algo interesante en el dominio podemos suscribirnos a ese evento para saber que ha ocurrido y hacer algo al respecto.

Volvamos al problema de la vista…

Una vista es un asunto de infraestructura, pues se trata de un interés de la interfaz de usuario. Así que empecemos por ahí. Una vista de lista típica consiste en una colección de representaciones de una entidad que contiene un subconjunto de sus datos. Aparte de eso, suelen necesitar filtros, ordenación y paginación.

Para las vistas existe un patrón clásico: Model-View-Controller o MVC. La parte que nos importa ahora es la M. La M puede entenderse también como ViewModel y en los frameworks MVC es típico que estén vinculados a la base de datos usando un patrón Active Record.

¿Y qué papel juega el ViewModel en nuestro ejemplo? Un ViewModel es básicamente un DTO que representa una entidad en esa vista específica. Para obtener ese ViewModel, o colección de ellos, podríamos pedirlo a una tabla específica en una base de datos si es el caso.

¿Pero no tenemos ya las entidades persistidas? ¿Para qué otra tabla? ¿No basta con añadir un método al repositorio y obtener las entidades y tal y cuál? Hay varios problemas con esto último. El principal es introducir asuntos de persistencia en el dominio. El rol del repositorio es proporcionar persistencia a las entidades, pero no resolver los problemas de las vistas. Tenemos el patrón Specification para obtener subconjuntos de las entidades, pero ese patrón no contempla paginación, ordenación o filtrado en el sentido de las vistas.

La solución es tener una tabla que esté asociada a una vista concreta, un ViewModel (o ReadModel, si lo prefieres más genérico), pero tenemos el problema de poblarla. También hay otra solución low-cost de la que hablaré más adelante.

Y aquí es donde entran en juego los eventos. Cada vez que se lanza un evento que pueda afectar a esa vista, como que se ha creado un nuevo pedido, o se ha actualizado, un suscriptor se encarga de actualizar el ViewModel si es necesario. Esto se puede considerar como una Proyección.

Así que si se ha creado un pedido, se añade una fila a la tabla del ViewModel que corresponda, o se hace lo que haga falta para mantenerlo en sincronía. De hecho, si guardas los eventos de modo que se puedan rebobinar y reproducir podrías reconstruir los ViewModels desde el principio de los tiempos. Y eso te sonará a Event Sourcing aunque la actualización de proyecciones solo es una parte de ese patrón.

Las grandes ventajas de este sistema son: simplicidad y rendimiento. Es bastante obvio que son tablas fáciles de mantener, y las queries son rapidísimas porque no traen datos de más, no tienen asociaciones ni otras cargas relacionadas.

Además, es fácil introducir ideas como filtros, ordenación, paginación, etc., precisamente porque trabajas con tablas básicas y tipos de datos simples.

El problema, porque nada es gratis, es mantener la consistencia.

Solución low-cost: para sistemas más sencillos, simplemente accede a la persistencia a través de otro servicio que lance queries simples contra las tablas que guardan tu entidad o agregado y te devuelva los ViewModel. No necesitas sincronización porque accedes a los mismos datos.

En este caso, nunca escribimos nada que no sea a través del repositorio en dominio.

En resumen: una vista es como una micro-aplicación MVC dentro de tu gran aplicación. Puedes introducir un caso de uso para gestionarla. A veces nos interesa que diferentes vistas usen el mismo caso de uso, como una vista web y un archivo CSV para exportar. El ViewModel simplifica el acceso a los datos y las operaciones que le interesan a la vista en la UI como filtros, ordenación o paginación, sin contaminar el dominio.

Notas

Patrones

Bajo acoplamiento

1

Diseño dirigido por dominio

Entidades

1Nota sobre DDD y programación funcional: Eric Evans deja claro que DDD nace en el paradigma de Orientación a Objetos.

2Nota, no es obligatorio usar UUID, pero actúa como si lo fuese por el bien de tu salud mental y la de tu aplicación.