En el campo del software, Jeff Bay introdujo una idea similar en un artículo de la publicación The ThoughtWorks Anthology: Essays on Software Technology and Innovation en 2008. Su propuesta consistía en establecer nueve restricciones al escribir código que tendrán el efecto de mejorar su estructura y diseño.
Del mismo modo que los ejercicios físicos calisténicos no están dirigidos a entrenar una habilidad concreta, las restricciones están pensadas para forzar ciertos buenos hábitos al escribir código orientado a objetos, contribuyendo a desarrollar un código de mejor calidad y a identificar los elementos que hacen bueno su diseño. Por otro lado, es importante tener en cuenta que la mayoría de ellas tienen sentido en este paradigma por lo que podrían no ser aplicables a otros, como el funcional. Aun así, creo que se pueden aprovechar como estilo de programación.
La lista de restricciones es la siguiente:
Un solo nivel de indentación por método
No usar la palabra clave ELSE
Encapsular todas las primitivas y strings
Encapsular las colecciones
Un punto por línea
No usar abreviaturas
Mantener todas las entidades pequeñas
No más de dos variables de instancia por clase
No usar getters/setters o propiedades públicas
Hay algo que llama la atención en esta lista. Son reglas que se aplican sobre características del código independientemente de cualquier propósito, ya sea del código, ya sea tuyo como desarrolladora.
Por ejemplo, la primera es fácil de identificar: ¿hay un bloque que tenga una indentación de dos o más niveles? Pues hacemos algo para aplanarlo, como puede ser extraer el bloque a un método privado. Al hacerlo, posiblemente te darás cuenta de que ese bloque estaba ocupándose de un detalle o aspecto de un algoritmo. Y lo interesante es que no necesitas pensar en eso a priori, pues es el hecho físico de cambiar la estructura del código dirigida por las reglas la que te permitirá descubrirlo.
Calisthenics y refactor
El objetivo de este libro es desarrollar cada una de las restricciones aplicada a un ejemplo de código y tratar de explicar cómo y por qué funcionan. Así que, en este libro tomamos como punto de partida la propuesta de Bay y la convertimos en un pequeño manual de estilo para que nos ayude a escribir mejor código.
Este libro podría considerarse también como un libro acerca de refactoring, ya que vamos a ver cómo aplicar las reglas de calistenia en un código existente.
De hecho, cuando quieres practicar calistenia, lo mejor es hacerlo para mejorar código que ya existe. Por lo general, puedes aplicarla sin mucho riesgo, de modo que puedes usar estas reglas en código que quieras llevar a producción.
Si perseveras en hacerlo, con el tiempo empezarás a escribir código que tiende a cumplir las premisas básicas. Por esta razón decimos que la calistenia de código nos ayuda a desarrollar un sentido de buen diseño: elementos pequeños, altamente significativos, con responsabilidades bien definidas, fáciles de entender y fáciles de reemplazar.
Solo un nivel de indentación
Esta restricción es bastante sencilla de entender, aunque puede que no tanto de aplicar.
La indentación nos ayuda a organizar visualmente el código de modo que cuando un fragmento está, por así decir, contenido en otro se muestra más adentrado en el cuerpo del texto. En Python, la indentación es lo que define los bloques de código mientras que en otros lenguajes estos bloques se definen usando algún tipo de marcador como las llaves {}, palabras clave como begin/end, etc.
El nivel de indentación está fuertemente asociado al nivel de abstracción. Con frecuencia, los bloques de código indentados se ocupan de un cierto nivel de detalle que no se corresponde al nivel de abstracción general del método que los contiene. La mezcla de niveles de abstracción hace que sea más difícil comprender el código debido a que tenemos que cambiar nuestro enfoque al entrar y salir de cada bloque.
Los bloques indentados aparecen en estructuras condicionales y en bucles. El problema de estos bloques surge cuando dentro de uno de ellos, nos encontramos con la necesidad de introducir una nueva condicional o bucle, resultando en una anidación que genera un nivel extra de indentación. Esto incrementa la mezcla de conceptos generales con detalles. Además, hace que tengamos que entrar y salir de distintas ramas del flujo de ejecución. En conjunto, el código así organizado se hace más difícil de leer, de comprender y de mantener en la cabeza.
Veamos un ejemplo no orientado a objetos, tomado de la kata Theatrical Players de Emily Bache:
En el ejemplo, se puede ver como el bucle for introduce un nivel de indentación en el código. Pero dentro de él podemos ver dos estructuras condicionales que añaden hasta dos nuevos niveles.
Para reducir a un solo nivel de indentación el código de un método lo más habitual es extraer la estructura anidada a un método privado, de manera que en su lugar quede una única línea con esa llamada. Este refactor es el conocido como extract method
Es este ejemplo, la función statement calcula el importe de una factura sobre varias actuaciones de una compañía de teatro. Para ello recorre la lista de actuaciones, calculando el importe de cada actuación basándose en características de la obra y de la audiencia y sumándolo todo. La primera estructura condicional contiene los detalles del cálculo del importe de cada actuación.
Si los niveles de anidación son varios podemos empezar por el primero y luego nos vamos moviendo más hacia adentro. Esto choca con la recomendación de empezar a refactorizar por la rama más profunda, pero en este caso parece mejor despejar cada nivel de abstracción desde fuera hacia adentro. Además, el refactor automático de extract method es lo bastante seguro como para poder realizarlo sin tests.
En nuestro ejemplo, el nuevo método sigue teniendo más de un nivel de indentación, pero volveremos a esto más adelante. Vamos a seguir aplanando la función statement.
El tipo de dificultades que nos podemos encontrar tienen que ver con el uso de variables que se inicializan fuera de la estructura condicional, pero que se modifican en ella. Un paso previo recomendable es agrupar el código estrechamente relacionado, por ejemplo, las líneas en las que se mencionan las mismas variables deberían ir juntas. De este modo, cuando vayamos a extraer la estructura condicional seremos más conscientes de esas dependencias.
La segunda estructura condicional hace lo que parece ser un cálculo de créditos o puntos para futuros espectáculos (volume_credits) y también podemos extraerlo. Como podemos ver, la condicional modifica el valor de una variable que se inicializaba fuera. Por tanto, incluimos todo en la extracción, teniendo en cuenta que volume_credits es una variable acumulativa:
Nos quedaría así:
Un truco simple cuando usas refactor automático es examinar los parámetros que necesita el nuevo método, ya que el análisis que hace la herramienta de refactor identificará todos los que se necesitan. Esto nos ayuda a descubrir variables temporales que tal vez no tendríamos que pasar o que deberían estar únicamente en el método extraído. Especialmente en el caso de haya que devolver su valor.
Para este ejemplo, tenemos que separar el cálculo parcial del total. Esta es la secuencia de pasos que he seguido para hacerlo manteniendo los tests en verde en todos los pasos:
En primer lugar, voy a introducir una variable performance_credits que contendrá el cálculo parcial:
volumen_credits solo debería actualizarse cuando se haya completado el cálculo parcial, así que lo muevo al final del fragmento:
En la condicional, actualizo performance_credits en lugar de volume_credits:
Ahora ya puedo extraer el cálculo limpiamente:
Si te fijas en la parte principal del cuerpo de la función statement verás que es mucho más claro y es fácil entender lo que ocurre en un nivel general. Basta con moverse a la función adecuada para poder acceder a los detalles de cada cálculo.
Queda más o menos así, una vez ordenadas las líneas:
Podría argumentarse que para poder extraer las estructuras condicionales y aplanar la indentación tengo que hacer algunos refactors de más. Pero precisamente ese es uno de los beneficios de intentar forzar la regla. Tengo que mejorar la organización del código para tener las condiciones adecuadas que me permitan aplicar la regla de un solo nivel de indentación.
Volvamos ahora a los niveles extra de indentación que aún no hemos tratado. Se han movido todos a la función calculate_amount:
Lo más fácil es mover las patas de las condicionales a sus propios métodos, como se puede ver a continuación.
Al fijarnos en el resultado, podemos observar varias cosas. Una de ellas es que el código de calculate_amount sugiere aplicar el patrón early return, que clarifica más aún el cuerpo del método, así como suprimir la palabra clave ELSE, tema que trataríamos en la siguiente regla. También nos abre la puerta a usar una estructura switch/case. Pero si profundizamos, también sugiere fuertemente la posibilidad de introducir orientación a objetos para beneficiarnos del polimorfismo. No lo vamos a hacer en esta ocasión, porque el objetivo del capítulo es centrarnos en una sola regla cada vez.
Este es el resultado hasta el momento. En cada función tenemos un solo nivel de indentación. No es el refactor definitivo, pero ha mejorado sustancialmente la organización y legibilidad del código.
A poco que profundicemos en este ejemplo podemos sentir que está pidiendo a gritos aplicar orientación a objetos, pero precisamente eso es algo que podemos empezar a vislumbrar después de haber ejercitado esta simple regla.
Por qué funciona
La regla de un solo nivel de indentación funciona porque nos ayuda a conseguir que cada método desarrolle su trabajo en un único nivel de abstracción, a la vez que separamos distintas responsabilidades. De este modo, puedes leer cada método y entender qué pasa, sin necesidad de distraerte con detalles que no son relevantes en ese momento. Si necesitas conocer cómo se implementa alguna de las fases no tienes más que revisar el método que se ocupa de ello.
Al separar el comportamiento de ese objeto en pasos implementados por métodos específicos será más fácil también identificar el papel de los colaboradores del objeto, si los hay, así como su aislamiento. De este modo, podremos detectar y solucionar más fácilmente posibles problemas de acoplamiento. A su vez, esta extracción a métodos privados puede ser el primer paso para identificar diferentes responsabilidades en una clase que podrían llevarse a nuevas clases.
Dado que son métodos privados no estamos afectando a la interfaz pública, por lo que es un refactor que podemos aplicar con seguridad.
Más allá
Veamos ahora como llegar más lejos aún con esta regla.
Separar iterador de iteración
Una forma de abordar los bucles es separar el iterador (el bucle) de la iteración (el cuerpo del bucle). Es decir, en lugar de tener un bloque de código, extraemos la totalidad del bloque a un método privado. De este modo, el cuerpo del bucle contendría una sola línea. Una de las ventajas de proceder así es que puede ayudarnos a identificar código que realmente pertenece a la clase del objeto que está siendo procesado en la iteración.
Aplicar esta separación en este ejemplo puede ser un poco complicado, dado que en el bucle for vamos acumulando ni más ni menos que tres variables: total_amount,volume_credits y result.
Pensemos si podemos hacer algo al respecto. Lo primero sería extraer una variable para almacenar la línea que estamos calculando en cada iteración:
Y ahora reunimos las variables acumuladoras:
Ahora podríamos intentar extraer la parte del cálculo a una nueva función. Sin embargo, en Python las inner functions no son visibles desde dentro de otras inner functions, así que tendríamos que pasarlas junto con los parámetros necesarios por lo que este paso no es muy viable. De nuevo, el refactor nos va mostrando que lo más efectivo sería introducir orientación a objetos para este caso.
Solo un if por método
En el libro Five lines of code, Christian Clausen propone llevar esta restricción un poco más allá. Además de que cada método tenga un único nivel de indentación, sugiere que solo haya una estructura condicional en cada método y que if debería ser siempre la primera línea. Vamos a ver algunos ejemplos en este código y cómo se podrían abordar.
En el primero, podemos ver que se paga un extra si la audiencia supera un cierto umbral. En caso contrario, no se incrementa.
Podríamos hacer una modificación temporal para verlo más claro:
Ahora podemos extraer el bloque condicional:
Esto hace que la condición quede como primera línea en extra_amount_for_high_audience_in_comedy, que es lo que buscábamos. Ahora limpiamos un poco el código para que quede menos redundante, removiendo variables temporales innecesarias.
Podemos aplicar un tratamiento similar para otros tipos de obras. El resultado sería este:
Y lo mismo en el cálculo de créditos:
Que quedaría así:
El resultado
Y este es el resultado final después de aplicar la regla de un solo nivel de indentación y las reglas extra:
Como puedes comprobar, se han introducido muchos else, lo que nos llevará a aplicar una nueva regla. Pero eso será en el siguiente capítulo.
No usar la palabra clave ELSE
Una estructura condicional puede dar lugar a varias ramas en el flujo de ejecución de modo que si se cumple la condición se sigue un camino, y si no se cumple… pues se sigue otro. O simplemente no se sigue ninguno y se continúa con la siguiente instrucción.
Pero, ¿qué problema hay con else? Al fin y al cabo, no indica otra cosa que seguir unas instrucciones específicas para el caso de que no se cumplan las condiciones requeridas en el if. Normalmente, el problema no es el hecho de usar elseper se, sino el contexto en el que lo usamos o la organización de código que produce. Se podría decir que utilizar else puede ser un smell, un síntoma de que algo podría estar mejor diseñado. Si nos obligamos a eliminarlo, podemos mejorar el código.
Condicionales sencillas, aún más sencillas
Una estructura if tiene este aspecto:
Usamos else cuando queremos ejecutar ciertas instrucciones en caso de no cumplirse la condición, de forma alternativa.
Esta estructura ya podría introducir algo de ruido a la hora de leer el programa. Como vimos en el capítulo anterior, nos interesa forzar un solo nivel de indentación como máximo para evitar la sobrecarga de seguir el código anidado. La introducción de else no añade un nivel de indentación extra, pero implica que tenemos que mantener en la cabeza dos flujos alternativos.
Esto se complica si tenemos que hacer seguimiento de variables que son inicializadas fuera de la estructura condicional, pero manipuladas en ella. También se complica la lectura si el tamaño de uno de los bloques es muy grande, ya que podría ofuscar el otro.
Por esa razón, se recomendaba aislar la estructura condicional en un método o función, de modo que el if fuese la primera línea y únicamente hubiese una condicional en ese método:
Y aquí la condicional aislada en su propia función o método:
Al aislar de esta manera las condicionales, tanto la rama del if como la del else retornarán al punto de llamada, ya que no hay más instrucciones que seguir. De hecho, podríamos retornar desde ambas ramas. Es lo que conocemos como patrón return early,
Esto hace redundante la palabra clave else, ya que no es necesario asegurar que la condición del if no se cumple.
Retomando el ejemplo del capítulo anterior, tenemos varias situaciones en las que se usa else que podríamos examinar. Recordemos el código:
Aquí tenemos un ejemplo:
Este caso es bastante sencillo porque el else es redundante:
Tenemos formas alternativas. Una de ellas consiste en usar el operador ternario, que funciona especialmente bien cuando queremos expresar un cálculo que se realiza de maneras diferentes. Utilízalo con precaución porque puede ser difícil de leer.
Otra forma de plantearlo es invertir la condición, tratando el caso residual primero. A esto se le suele llamar cláusula de guarda. Es especialmente aplicable si se trata de verificar precondiciones de los parámetros que llegan al método o función. De esta forma, centras la atención en la rama más significativa.
Cualquiera de las tres técnicas te permite suprimir el else. La más adecuada dependerá del aspecto que necesites acentuar. Para este ejemplo podrían funcionar las tres bastante bien y resulta difícil decidirse por una de ellas. Quizá en este caso optaría por la condicional invertida.
De este modo, las tres funciones que contienen condicionales simples quedarían así:
Condicionales complejas
Tenemos otro ejemplo interesante aquí:
Se trata de una serie de condicionales encadenadas a través de la clave else o else if. La estructura condicional maneja un cierto número de condiciones de tal manera, que si no se cumple la inicial, tenemos que verificar si se cumplen otras y actuar en consecuencia.
Esta estructura encadenada se entiende mejor usando switch, lo que esconde el else, aunque realmente no lo elimina. Python no tiene switch, y la forma de hacerlo es encadenando if/elif/else.
De nuevo, podremos usar return early para simplificar la estructura. Primero introducimos el return.
Y a continuación, eliminamos los else:
Por qué funciona
Eliminar else nos obliga a pensar bien nuestras estructuras condicionales. Una estructura condicional siempre hace al menos dos cosas: decidir si se cumple la condición, hacer algo si es así. En el caso de else, hay que añadir una tercera cosa: la acción alternativa.
De hecho, en orientación a objetos, la mera presencia de una estructura condicional puede significar un problema de diseño. Esto ocurre, por ejemplo, cuando la condicional verifica alguna propiedad de un objeto (o de algún concepto del programa que potencialmente pueda ser un objeto). En ese caso, se pone de manifiesto la necesidad de polimorfismo. Nuestro último refactor lo deja muy claro.
Cuando se toma una decisión basada en el tipo de un concepto, debería abordarse mediante polimorfismo.
Sin embargo, cuando la decisión se basa en un valor, podríamos recurrir a otros enfoques
De todos modos, la introducción de la orientación a objetos vendrá de la mano de las siguientes reglas, que consisten en empaquetar todas nuestras primitivas y colecciones en objetos. Es decir, representar los conceptos usando objetos.
El resultado
Después de eliminar la palabra clave else, el código queda así:
Parece muy claro que conceptos como obra (play) y actuación (performance) están pugnando por salir. Y alguno más. Lo veremos en el capítulo siguiente.
Empaquetar primitivas en objetos
Los lenguajes de programación proporcionan tipos de datos básicos, llamados primitivos, con los que podemos representar los diversos conceptos que maneja un programa. Sin embargo, esta representación suele ser imperfecta.
Pensemos por ejemplo, en un precio. El precio se puede representar con un número, pero hay varias características de los números con los que representamos precios que son importantes: son valores positivos, tienen decimales con reglas específicas de redondeo, y suele ser importante conocer la unidad monetaria, entre otros detalles.
Estas características no las proveen los tipos numéricos primitivos habituales. Por esa razón, un tipo específico, que puede estar basado en uno primitivo, pero que encapsule esas reglas es mucho mejor solución. Basta con realizar una encapsulación básica para empezar a obtener beneficios, ya que eso oculta al resto del programa los detalles de implementación del tipo y nos permite que evolucione sin afectar al resto del código. A medida que introducimos comportamiento y validaciones en ese objeto, el programa se beneficia automáticamente.
Encapsular tipos primitivos simples
Así que volvamos a nuestro ejemplo, en el que tenemos un montón de posibles casos. Para empezar, nos encontramos con los parámetros que se pasan a la función statement. Estos nos presentan algunos problemas particulares porque contienen colecciones de cosas, así que vamos a dejarlo para la próxima regla.
Lo primero que nos encontramos es total_amount, que representa el importe de la factura y que va acumulando parciales.
total_amount representa una cantidad de dinero. En este ejemplo, la unidad monetaria resulta ser el centavo como se puede apreciar en la forma en que se usa la función format_as_dollars. Básicamente, necesitaremos dos comportamientos: poder acumular y obtener el importe acumulado hasta el momento. Este nuevo tipo se podría llamar Amount.
Para no tener los test rotos mucho tiempo voy a introducir el cambio en paralelo, añadiendo la nueva clase, pero sin introducir el cambio hasta el último momento. Por supuesto, puedo hacer esto con TDD porque es código nuevo.
Ahora, añadiré un método para acumular importes. Aprovecharé para hacerlo inmutable.
Con esto tengo suficiente para empezar a usarlo. Para ello introduzco una variable invoice_amount.
Y, finalmente, solo tendría que reemplazar la variable total_amount en la línea que imprime el importe final. La idea es que todos los cambios anteriores ya estén mezclados, de modo que este nuevo cambio ocurra en un único commit y se pueda revertir fácilmente en caso de que falle.
De hecho, los tests de statement siguen pasando perfectamente, por lo que podemos quitar total_amount ya que ha dejado de usarse.
Una cosa interesante es que this_amount también debería ser un Amount, así que tendría sentido examinar calculate_performance_amount, para que devuelva un tipo Amount. Eso nos lleva a una serie de cambios con una mecánica muy similar a la que hemos seguido. Introducimos el código nuevo en paralelo y lo consolidamos en un commit. Finalmente, usamos el nuevo cálculo aislado en un único commit para que deshacerlo sea sencillo. Una vez que confirmamos que no se ha roto nada, eliminamos el código viejo.
En este caso, lo que voy a hacer es introducir un objeto Amount en las funciones que realizan el cálculo y, provisionalmente, dejaré que todavía no devuelvan el tipo Amount, sino el primitivo calculado con Amount. En una segunda fase adaptaré el código llamante para que espere el tipo Amount. He aquí un ejemplo:
El primer paso es calcularlo en paralelo:
Una vez hecho un commit con esos cambios, utilizaré el nuevo cálculo, pero sin devolver todavía el objeto:
Como los tests siguen pasando puedo consolidar el cambio y eliminar el código que ya no uso.
Por supuesto, puedo evitar el uso de variables temporales:
El mismo cambio se puede aplicar en muchos lugares. Usaremos el mismo procedimiento, aunque no lo voy a mostrar para no alargar el capítulo innecesariamente. Así es como quedará, teniendo en cuenta que todavía estoy dejando que las funciones retornen el primitivo.
Ahora iré desde dentro hacia afuera cambiando el tipo retornado para dejar de usar el primitivo. Iré paso a paso, para asegurarme de que lo hago bien, pasando los tests cada vez. Este es el primero:
Y sigo paso a paso hasta que los cambio todos. La idea es que solamente use Amount.current cuando sea necesario para imprimir la factura.
Como se puede ver, el código no ha cambiado demasiado y seguramente hay espacio para muchas mejoras, pero tenemos que proceder de manera sistemática. Así que vamos a buscar otro primitivo que podamos reemplazar.
volume_credits tiene un funcionamiento similar a Amount, pero significa una cosa distinta, así que vamos a introducir una clase Credits, que representará ese contexto. Y usaremos la misma aproximación: introducir la nueva clase, usarla en paralelo y, finalmente, sustituirla.
Los cambios en el código los hacemos de la misma manera que antes. El resultado será más o menos este:
Nuestro siguiente candidato es result, que es un string que va acumulando las líneas que se imprimirán en la factura. De hecho, podríamos incorporar el concepto de Printer como objeto encargado de imprimir las líneas que se le pasan, en lugar de un simple almacén de líneas para devolver al final. Queremos que funcione más o menos como indica este test:
De momento lo implementamos así, que es más o menos como está en el código original y será suficiente para lo que necesitamos:
Par integrarlo, procedemos del mismo modo que antes. Primero lo introducimos en paralelo y dejamos que el último cambio sea muy simple. El resultado, una vez eliminado el código anterior es este:
Encapsular estructuras de datos nativas
Nos quedan varios objetos interesantes. En particular Play y Performance, que son centrales en este dominio. Veamos cómo los podemos tratar.
En principio, estos objetos están tratados como diccionarios (o hash, o array asociativo, según el lenguaje). La tentación es intentar crear desde cero un objeto que reproduzca esa estructura. Sin embargo, vamos a seguir un enfoque más simplista. Por el momento solo vamos a encapsular esos diccionarios y añadir métodos que nos permitan acceder recuperar los valores de sus claves.
Una vez hecho esto, que será el primer paso, podremos hacer evolucionar la estructura interna sin que el resto del código tenga que preocuparse de ello. La razón para hacerlo así es evitar mezclar distintos objetivos en una única acción de refactor.
Después de examinar el código pienso que voy a empezar por Performance. En el bucle de la función statement se recorren las distintas Performances que se van a facturar y se opera con sus datos. En principio, una performance tiene las siguientes propiedades:
playID, que hace referencia a la obra representada
audience, que representa la cantidad de pública asistente
Así que introduciré la clase Performance, que tendrá por el momento dos métodos públicos: play_id y audience. En esta ocasión no voy a hacer tests, ya que son métodos triviales y su comportamiento quedará cubierto por los tests que ya tenemos.
Sospecho lo que estás pensando, pero de momento lo único que quiero es estar seguro de que el cambio funcionará. Ten en cuenta que este ejemplo es muy sencillo. En situaciones en que las estructuras de datos sean más complejas, este paso previo sirve para explorar las responsabilidades del objeto sin preocuparnos de su estructura interna.
Ahora toca introducirlo. Será aquí:
Es ahora cuando podemos empezar a usarlo. Primero, en este nivel de abstracción:
Y ahora viene algo interesante. Tenemos un par de funciones a las que les pasamos las variables perf (que representa una Performance) y play para hacer cálculos con ellas:
De hecho, podemos ver que esta función llama a otras que utilizan perf como único parámetro. Esto nos está indicando que este comportamiento es propio de Performance. Es decir, sería responsabilidad de Performance calcular el importe facturable. Básicamente, me estoy refiriendo a estas funciones:
¿Cómo voy a hacer este cambio? La verdad es que se me ocurren un par de maneras, aunque muy similares. El objetivo es copiar y adaptar el código que ahora está en funciones internas en statement para que sean métodos en Performance. La dificultad está en cómo hacer esto sin romper el test que tenemos.
Vamos con la primera forma. El primer paso es copiar el código de las funciones en Performance y adaptarlo de manera que no haya errores. Debería quedar más o menos así:
Ahora tenemos que pasar performance en vez de perf a la función en la línea:
En estos casos, lo que suelo hacer es añadir un nuevo parámetro y luego reemplazar su uso, hasta que el viejo parámetro queda sin usar. Cuando verifico que todo funciona correctamente, elimino el viejo.
En este punto puedo hacer commit antes de realizar el cambio importante, que sería hacer que performance ejecute el cálculo:
He hecho el cambio y los tests siguen pasando, así que puedo eliminar el parámetro perf y también las funciones internas que ya no necesito.
¿Empieza a tener mejor pinta? Parece que sí. Hacemos lo mismo con calculate_performance_credits. No voy a poner todo el detalle del proceso, pero es la misma idea: mover el código a Performance, adaptándolo y cambiando los usos de las funciones internas por llamadas al objeto. Finalmente, eliminar el código que no necesitamos.
Así es como queda Performance:
Y ahora reemplazamos las llamadas a las funciones por Performance:
El panorama se ha ido despejando al introducir objetos que han atraído comportamiento y eso que aún nos queda por traer a colación el objeto Play.
Pero ahora nos fijamos en estas líneas. Hay una falta de simetría que ralla un poco:
Está claro que calculate_performance_amount es un comportamiento de Performance, es hora de llevarlo a su lugar. Hacemos exactamente lo mismo. Copiar y adaptar. Luego reemplazar.
Un detalle que quiero destacar de Performance es el uso de la auto-encapsulación. Esto consiste en no acceder directamente a las propiedades de una clase, sino a través de métodos que podrían ser privados. De este modo, el resto del código de la clase no tiene que saber nada acerca de su estructura y me da libertad para cambiarla en cualquier momento, como veremos más adelante.
Mejoremos un poco el nombre de las cosas:
Nos queda introducir un objeto para representar una obra, que será Play. Por supuesto, hay una relación estrecha entre Performance y Play pero, de momento, no nos vamos a ocupar de eso. Simplemente, queremos introducir el concepto y luego, ya veremos a dónde nos lleva.
Lo primero que hago es revisar qué cosas necesitamos de Play:
name, para crear líneas de concepto en la factura.
type, para saber qué tipo de obra es, ya que implica precios diferentes.
Esencialmente, hacemos lo mismo que con Performance. Empezamos simplemente encapsulando la estructura de datos de la manera más simple posible:
Como primer paso, reemplazamos la representación actual por el objeto. Play se usa sobre todo en Performance, pero hay un uso en statement que, de momento, necesitamos tener en cuenta:
Esta es únicamente un primer paso. Dentro de un momento, veremos algunas ideas para proseguir con el refactor basándonos en las oportunidades que nos proporciona haber introducido objetos.
Por qué funciona
La regla de encapsular todos los primitivos en objetos funciona porque, de entrada, nos ayuda a separar responsabilidades entre los diversos conceptos que participan en el programa. Además, contribuye a ocultar algunos detalles de implementación, haciendo más fácil entender qué está pasando.
Los objetos nos permiten encapsular reglas de negocio y aislar los detalles de implementación entre las distintas partes del código. Esto ayuda, además, en que esas mismas partes puedan evolucionar de forma independiente, sin afectar al funcionamiento del conjunto del programa. Ninguna parte del programa necesita saber, por ejemplo, los detalles estructurales de Performance o Play. Simplemente, les pasan mensajes para que proporcionen la información solicitada. La forma en que se calcula no es importante para el objeto que envía el mensaje, pero igualmente la obtiene.
A medida que hemos ido introduciendo objetos, hemos podido reducir el tamaño de la función statement y que su código sea mucho más expresivo. Por supuesto, es mejorable, pero ahora no están mezclados la mayor parte de detalles. En conjunto, hay mucha más cantidad de código, pero es mucho más legible y fácil de mantener.
Esto ocurre porque los objetos funcionan como atractores de comportamiento. Una vez que descubrimos un objeto que participa en el programa, resulta fácil asignarle responsabilidades y extraerlas del código inicial. Por otro lado, los objetos nos ayudan a garantizar que los datos que encapsulan cumplen las reglas de dominio requeridas. No necesitamos verificarlo constantemente.
Más allá
Agregación de objetos
Al introducir objetos se va clarificando el escenario del programa y las relaciones entre los distintos conceptos. En nuestro ejercicio, por ejemplo, se aprecia muy bien que Play es un elemento de Performance y, salvo por conocer el nombre de la obra para poder imprimir la factura, la función statement no necesita saber ni que existe.
Así que podemos transformar Performance para usar Play. Sin embargo, antes nos vendría bien cambiar el modo en que Performance guarda su información. Es ahora cuando se pueden apreciar los beneficios de la auto-encapsulación. Solo tengo que cambiar unas pocas líneas:
De esta forma, es más fácil añadir una nueva propiedad:
Y dar soporte al cambio en la instanciación, así como en el único uso directo que hace statement de Play.
Nos queda eliminar el paso de Play a los métodos amount y credits. Pero será bastante fácil:
Y tras eso, eliminar el parámetro innecesario:
Sí, lo sé. Se pueden ver algunas cosillas cuestionables todavía. Vamos a seguir permitiendo que sean las reglas de Calisthenics las que nos guíen en el proceso y veremos si se arreglan o no.
El resultado
El código ha evolucionado muchísimo tras aplicar la regla de encapsular primitivos en objetos. Sin embargo, todavía nos quedan algunos por atacar. Particularmente invoice y plays, pero los dejaremos para la próxima regla que nos pide hacer encapsular colecciones.
Si observamos el código desde el punto de vista de refactoring está claro que aún nos queda mucho trabajo por hacer y algunos smells son evidentes y no están siendo tratados. Esto tienen un motivo en el contexto de estos capítulos y no es otro que queremos ver si aplicar las reglas de forma sistemática nos conduce eventualmente a un mejor diseño. Hasta ahora creo que puede decirse que sí, con algunas salvedades, pero también es cierto que estamos aplicando cada regla una por una. En otras circunstancias estaríamos usando las reglas allí donde se viesen aplicables sin importar el orden.
En cualquier caso, en este momento podemos observar algunos efectos positivos, ya que las responsabilidades se han ido distribuyendo en objetos y funciones.
Encapsular colecciones
Se trata de encapsular en un objeto toda estructura de datos que represente una colección de tal manera que la única propiedad de este objeto sea esa misma estructura, con los métodos que necesitemos para tener acceso a los datos. Y en el fondo no es más que una extensión de la regla anterior. Unificadas ambas, podríamos decir que cualquier estructura de datos nativa del lenguaje debería ser encapsulada, da igual lo simple (primitivos) o compleja que sea (colecciones).
El motivo es aislarte de la estructura de datos de tal forma que el resto del programa no esté acoplado a la misma. Esto nos permite cambiar la estructura sin tener que tocar el resto del código cuando tengamos alguna razón para ello.
En el ejemplo que estamos usando en esta serie tenemos un par de buenos casos: plays y performances, dentro de invoice.
Colección con acceso por clave
Este es el caso de plays. Accedemos a un elemento de esta colección dada una clave, que en este caso es el ID de la obra. La responsabilidad de plays en este sistema es actuar como una especie de catálogo en el que consultar las obras que la compañía puede representar. Simplemente, necesitamos un método get_by_id, que nos devuelva la obra solicitada.
Únicamente tenemos un uso y es fácil reemplazarlo:
Fíjate que no se trata de refactorizar la estructura en sí y cambiarla por otra que pueda ser más eficiente o apropiada. Se trata simplemente de no usar directamente ninguna estructura nativa, como si fuese una dependencia de terceros a la que no queremos acoplarnos.
Recuerda también aplicar YAGNI (no lo vas a necesitar), e introduce solo los métodos que tu código necesite para funcionar.
Colección iterable
La única diferencia significativa entre el caso anterior y este, en el que vamos a encapsular la colección de performances, es que queremos poder iterar los elementos de esta colección, ya sea mediante un bucle for como el que tenemos en el ejemplo, ya sea mediante otro enfoque.
En Python podemos hacer iterable una clase definiendo el método __iter__ para que devuelva una clase iteradora, la cual debe contener el método __next__:
En el cuerpo de statement hacemos de esta manera:
Este cambio ha sido un poco más elaborado y ha conllevado algunas modificaciones interesantes. Por ejemplo, la instanciación de Performance ocurre dentro de Performances, así que statement ya no necesita conocer cómo se construye un objeto Performance.Luego profundizaré en algunas consecuencias de esto.
Como he mencionado antes, lo único que hemos hecho ha sido mover la estructura de datos original (un diccionario) dentro de una nueva clase. De esta forma, el código de statement, no conoce los detalles de implementación de Performances (o de Plays) pero sigue pudiendo acceder a la información que necesita. En el futuro podríamos cambiar esto sin necesidad de afectar a statement, lo que es una ventaja importante.
Por eso, aunque ahora mismo el código dentro de Performances nos parezca menos que bueno, podremos cambiarlo en cualquier momento sin miedo de romper cosas en múltiples lugares. Los cambios ocurrirán únicamente en un sitio (dentro de Performance), maximizando la mantenibilidad y minimizando la dispersión de los errores potenciales.
Por qué funciona
Al igual que ocurre con la regla anterior, encapsular colecciones nos permite desacoplarnos de la estructura nativa de datos. Esto es una gran ventaja porque nos aporta libertad a la hora de cambiar esta estructura y la gestión de los datos en ella.
Además, este tipo de cambios suele generar algunas ventajas más. Las estructuras nativas están diseñadas para cubrir numerosos casos de uso, por lo que son genéricas y pueden incluir métodos que no vamos a necesitar o que introducen confusión a la hora de utilizarlos. Al encapsular en una clase, podemos definir cómo queremos que el resto del programa interactúe con ella de forma inequívoca, usando incluso un lenguaje apropiado a nuestro dominio.
Por otro lado, estos procesos de encapsulación ayudan a descubrir y modelar mejor relaciones entre conceptos, sugiriendo dónde deben ir las distintas responsabilidades.
Más allá
A medida que aplicamos las reglas de Object Calisthenics el código no solo va tomando mejor forma, sino que también desvela áreas que pueden mejorar.
Esto ocurre porque, en general, las reglas nos fuerzan a organizar mejor el código. No arreglan los problemas, pero contribuyen a despejar el paisaje de una forma parecida a lo que ocurre cuando, por ejemplo, organizamos las piezas de un puzzle por colores o texturas antes de empezar. Se podría decir, que gracias a esta manera de trabajar conseguimos dividir un problema grande en partes manejables.
Así, por ejemplo, tras el último cambio podemos ver que invoice es la última estructura de datos nativa que nos queda por arreglar. Pero también vemos que podríamos mejorar cosas en la forma en que instanciamos Performance.
Encapsular estructuras de datos
Como he mencionado más arriba, tanto la regla “Encapsular colecciones” como la del capítulo sobre “Encapsular primitivos” son dos caras de una misma moneda: encapsular cualquier estructura de datos nativa. Esto es, cualquier concepto que aparece en nuestro dominio debería ser representado por un objeto que se puede implementar usando la estructura de datos que más nos convenga, pero sin que el resto del código tenga que saber qué estructura en concreto estamos usando.
En este ejercicio he dejado invoice para el final para analizarlo con calma. En principio, un objeto Invoice nos debería proporcionar el nombre del cliente (para imprimir la factura) y la lista de actuaciones.
Y reemplazar sus usos en el código resultaría trivial:
De entrada, es fácil ver que Invoice nos pide más responsabilidades. Por ejemplo, la instanciación de Performances debería ocurrir en Invoice. Podríamos hacerlo así:
Y usarlo de esta manera:
Ahora está claro que la lógica para calcular invoice_amount y volume_credits está reclamando fuertemente formar parte de Invoice, cosa que tiene su complicación dada la forma en que se imprime la factura. Ya llegaremos a esto, pero ahora se ve claramente que hay dos responsabilidades diferentes: el cálculo de las líneas y totales de la factura y la impresión de las mismas. Nuestro problema es que ahora aparecen entrelazadas.
¿Hay algo que podamos hacer aquí? Una posibilidad es eliminar variables temporales, lo que reduce bastante el ruido, aclarando algunas cosas, pero ensuciando otras.
Evolución interna de los objetos
Hemos dicho que al encapsular estructuras de datos en objetos, la evolución interna de estos se hace de forma transparente para el resto del código. Esto nos permite hacer cambios sin romper funcionalidades, especialmente si estamos protegidas por tests.
Vamos a ver unos ejemplos.
Tras la transformación anterior, alguien podría argumentar que llamamos dos veces a performance.amount(), lo que podría tener consecuencias en, ejem, performance.
Si esto te supone mucho problema, un patrón memoization podría ayudar. Básicamente, se trata de mantener una cache del cálculo, de la cual el código que llaman no tiene que saber ni que existe. Por ejemplo, esta implementación bastante ingenua:
Como puedes ver, al tener objetos con responsabilidades bien definidas y un contrato claro con sus usuarios, introducir mejoras es muchísimo más fácil y seguro.
Otro asunto interesante es que cuando instanciamos Performance, seguimos pasando la colección completas de obras. Pero no tenemos por qué hacerlo así, ya que ahora es más fácil montar Performance con la obra (Play) que le corresponde. Este es el código que tenemos ahora:
Y este el cambio que proponemos:
Mientras que Performance podría quedar así:
Pero entonces resulta que podemos tener un constructor mucho más natural:
Y usarlo de esta otra forma:
Resultado
Object Calisthenics nos está ayudando a despejar el diseño del código, identificando objetos y repartiendo responsabilidades. Gracias a ello tenemos un código que, aunque es más grande, está organizado en objetos cada vez más especializados en sus tareas, de modo que la comprensión del sistema es mejor, a la vez que se hace más mantenible y, como acabamos de ver, incluso más fácil de optimizar.
Un punto por línea
Esta regla nos pide no encadenar llamadas a objetos proporcionados por otros objetos, de tal forma que solo tengamos un punto (una flecha en PHP) en cada línea de código. Puede parecer fácil de aplicar, pero vamos a poder identificar varias situaciones en las que la regla no es relevante, así como diferentes soluciones cuando sí lo es.
Las interfaces fluidas son correctas
Las interfaces fluidas no se ven afectadas por esta regla. Las interfaces fluidas devuelven el mismo objeto al que se pasa el mensaje, por lo que podemos seguir enviándole mensajes sin límite, lo que parece una oportunidad de aplicar la regla. Pero no lo es. En todo caso, es cierto que poner un punto por línea mejora mucha la legibilidad. El objetivo, y ventaja, de la interfaz fluida es poder enviar varios mensajes a un mismo objeto en un orden dado y que, además, se pueda entender como una operación unitaria.
En nuestro código, hacemos algo así con Amount, aunque cada vez se devuelva una instancia distinta es semánticamente el mismo objeto:
Filtración de propiedades
Fijémonos ahora en esta línea:
Para saber el título de la actuación, tenemos que pedirle a Performance la obra y obtener su título. De este modo, se revela un detalle de implementación de Performance que el resto del código no tiene por qué conocer. El comportamiento que se quiere de Performance es que sea capaz de decirnos el título de la obra que se representa, da igual si lo tiene guardado, si le pregunta a Play o si tiene alguna otra forma de obtenerlo o construirlo.
Por eso, una forma más adecuada sería:
De tal forma que ahora el mundo exterior no tiene ningún detalle sobre cómo hace Performance para proporcionar el título de la obra representada:
De este modo, el resto del código reduce su acoplamiento de Performance y esta puede modificar la forma en que obtiene el título sin afectar a sus consumidores.
Con todo, en este caso concreto podría haber otras soluciones, pero no voy a tratarlas en este momento, ya que me estoy limitando a aplicar las reglas de Calisthenics. Pero, en cualquier caso, creo que se ve muy bien cómo aplicar una regla va desvelando mejores soluciones, pero también problemas de diseño más profundos que requieren soluciones más elaboradas. Es decir: intentar aplicar la regla nos lleva a pensar más a fondo en ciertas decisiones de diseño.
En este caso, la solución es aceptable porque tiene sentido que Performance tenga como una de sus responsabilidades saber el nombre de la obra representada.
Más filtración de conocimiento
Veamos este fragmento:
Aquí tenemos un problema aparentemente similar. El método Performance.amount() nos devuelve un objeto y statement invoca un método en ese objeto devuelto. ¿Podemos aplicar la misma solución que antes?
Aparentemente sí, añadiendo a Performance un método que nos proporcione ese valor, algo así como:
Si lo pensamos un poco a fondo, veremos que no es nada correcto. Y eso es porque, de hecho, el método Amount.current() no debería existir, ya que en realidad expone una propiedad del objeto Amount. El método existe porque necesitamos obtener el primitivo contenido en el objeto. En otras palabras: intentar aplicar esta regla va más allá de simplemente encapsular el código en un nuevo método. Debería hacernos reflexionar sobre el diseño.
Una mejor solución es delegar y pasar el objeto a alguien que sepa comunicarse con él, Con todo, todavía presenta problemas, pero los tendremos que examinar en otro momento:
Un caso muy sutil
¿Notas algo problemático aquí?
Pues es un caso muy sutil de violación de esta regla. statement recibe objetos Performance que no tendría que conocer. Es una situación similar a la que acabamos de describir en el apartado anterior.
Podríamos abordarla así, pero los problemas son evidentes.
Tenemos que pasar variables que serán retornadas, aparte del objeto Performance. Y para completarlo, el nuevo método devuelve dos valores.
Hay varias razones por las que está pasando esto. Por un lado, el hecho de Invoice sea, por el momento, un objeto muy anémico, ya que debería ser responsable de calcular tanto el importe total como los créditos. Por otra parte, en el bucle están pasando varias cosas: se calculan los importes parciales, se van acumulando los dos totales y además se envían las líneas para imprimir.
Nos conviene separar las responsabilidades. Primer paso:
Segundo paso. Pongamos juntas las cosas relacionadas:
Se debería ver claro que esta lógica pertenece a Invoice y la podríamos pasar sin mucha dificultad.
Y así quedaría statement, después de limpiar un poco el código.
Por qué funciona
Esta es una regla que nos remite al Principio de Mínimo Conocimiento o Ley de Demeter y su objetivo es evitar acoplarnos a detalles internos de otros objetos. Nos fuerza a considerar los objetos como cajas negras con las que nos podemos comunicar, pero no saber cómo funcionan por dentro.
Cuando un objeto usa otro lo hace a través de su interfaz pública. La interfaz pública define los mensajes que un objeto puede recibir y las respuestas que puede devolver. Este es el máximo de conocimiento que un objeto debería tener sobre otro para minimizar el acoplamiento. Todo conocimiento a mayores incrementa el acoplamiento. Ese conocimiento incluye saber cómo comunicarse con objetos que son devueltos. La acción del consumidor debería limitarse a pasar ese objeto para que sea empleado en otro sitio.
En general, que haya puntos del código en que aplicar esta regla nos revela errores de diseño. Le estamos pidiendo a objetos comportamientos que no les corresponden, usando un conocimiento íntimo de su estructura.
El resultado
Por un lado, esta regla nos ayuda a mover responsabilidades a su lugar adecuado. Pero también suele destapar problemas que requieren reconsiderar nuestro diseño. No basta con introducir un método para ocultar una llamada encadenada.
Por eso, el resultado en este momento resulta un poco insatisfactorio. Tendremos que esperar a las reglas restantes para alcanzar mejores soluciones.
No usar abreviaturas
Este capítulo debería ser bastante breve, ya que no hay muchos casos en nuestro ejercicio de ejemplo. No obstante, forzaremos algunos ejemplos para ver los problemas del uso de abreviaturas.
La regla nos pide no usar abreviaturas para nombrar variables, objetos o funciones. El objetivo, por supuesto, es que el código sea lo más autoexplicativo posible. Si nos encontramos una abreviatura puede ocurrir que no conozcamos la referencia, puede que sea ambigua y no pueda entender bien el significado ni siquiera por el contexto, o puede ser simplemente confusa.
Abreviatura por conflicto de nombres
Aquí tenemos un ejemplo de uso de una abreviatura. En este caso para no generar un conflicto de nombres entre el parámetro que pasa los datos y la variable que contiene el objeto Invoice. Este tipo de atajos vienen de no tomar suficiente tiempo para pensar un nombre adecuado.
La pregunta es, ¿quién de los dos tiene el derecho a llamarse propiamente invoice? A medida que hemos ido aplicando reglas e introduciendo objetos, también necesitamos cambiar nombres. Al principio, invoice designaba una estructura de datos que representaba una factura. Sin embargo, al introducir el objeto Invoice, el parámetro pasa a ser un simple transporte de datos.
Por esa razón, realmente tiene más sentido hacer algunos cambios en los nombres. Esta es una posible solución:
Lo que nos dice un nombre
La abreviatura inv puede ser confusa si no tenemos contexto. Por ejemplo, es habitual que signifique inverso, así que siempre es preferible poner nombres completos, significativos e inequívocos. Es preferible pasarse por nombre largo que por nombre corto.
En Performance tenemos el método extra_amount_for_high_audience_in_comedy, cuyo nombre es extremadamente largo. Sin embargo, es inequívoco y dice exactamente lo que hace. A veces, el contexto nos puede proporcionar suficientes pistas. Este método es llamado desde calculate_amount_for_comedy, por lo que podríamos considerar acortarlo a extra_for_high_audience. Pero existe otro método
de nombre similar en la misma clase: extra_amount_for_high_audience_in_tragedy. Así que para diferenciarlos deberíamos mantener la referencia al tipo de obra.
Por supuesto, en realidad estos nombres nos están insistiendo en la necesidad de abordar el polimorfismo de Play, pero es algo que vamos a dejar para otro capítulo más adelante. La lección aquí es que reflexionar sobre los nombres nos ayudará a alcanzar un mejor diseño.
En cualquier caso, si un nombre resulta incómodo por ser demasiado largo, siempre tienes la oportunidad de refactorizar.
Abreviaturas aceptables
Algunas abreviaturas son de uso común. Por ejemplo, vat por value added tax.
Convenciones problemáticas
Existen algunas convenciones que usan nombres abreviados o especialmente cortos. Un ejemplo son los bucles, en los que se suelen usar nombres de variables como i,j o k. En su lugar es recomendable usar alternativas: index,position,counter, son mucho más explícitas y más difíciles de confundir.
Frente a:
En general, usar variables de una sola letra es confuso. ¿Qué es p? Incluso teniendo el contexto, una variable de una única letra nos obliga a pensar dos veces.
Además, es poco práctico. Si tienes que hacer una búsqueda de texto para encontrar la variable puede ser una odisea. Dentro del archivo nos salva que para tareas de refactor los IDE suelen usar el árbol sintáctico, pero si la búsqueda es de texto normal… ¡Buena suerte!
Esto ocurre también con nombres cortos, pero demasiado genéricos, como get,add, etc., que son comunes a infinidad de librerías.
Por qué funciona
No usar abreviaturas nos fuerza a pensar nombres significativos, lo que ayuda a que el código se explique mejor por sí mismo. Esto permite que sea más fácil incorporar más personas a los proyectos y hacerlo más mantenible en el largo plazo. Puede que con el tiempo nos olvidemos de lo que significaban las abreviaturas, por lo que usar nombres completos será una ventaja.
Mantener todas las entidades pequeñas
Esta regla suele generar discusión porque vamos a poner un límite totalmente arbitrario al tamaño de las entidades de código. Esto se refiere a clases, al número de métodos, al cuerpo de funciones, al número de archivos en un paquete, etc. Por ejemplo, esta es una propuesta más o menos típica:
10 archivos por paquete o carpeta
50 líneas por clase
5 líneas por método o función
2 argumentos por método o función
Así que se trata de recorrer el código buscando áreas que superen estos límites.
El objetivo, como ocurre en todas las reglas de Calisthenics, es que tratar de forzar la aplicación de las reglas nos traiga como resultado un código mejor diseñado, más fácil de entender y de mantener. En el caso de esta, lo que buscamos obtener es un sistema de objetos pequeños muy simples.
Lo cierto es que después de todos los cambios resultado de aplicar las reglas anteriores, nos encontramos con relativamente pocos casos problemáticos. Pero alguno hay.
Paquetes y sub-paquetes
Por ejemplo, el paquete domain, que contiene casi todo el código que hemos generado, no llega a 10 archivos. En parte es porque tenemos algunos archivos que contienen dos clases, algo que no está recomendado en todos los lenguajes. Puedes verlo como una forma de contribuir a esta regla, haciendo que el módulo de Python se pueda considerar como un sub-paquete y forzando que no contenga más de 10 clases o funciones.
En general, en el caso de encontrarnos con paquetes de más de 10 archivos, deberíamos plantearnos agruparlos por algún criterio en sub-paquetes cohesivos.
Clases grandes
Tenemos una clase que tiene más de 50 líneas. Performance contiene gran parte de la lógica del programa, pero: ¿podemos reducir su tamaño? O bien, ¿necesita realmente ser tan grande? Además, el método amount tiene unas 10 líneas, con lo cual también supera el límite de cinco que habíamos definido.
El problema de Performance es que se ocupa de varias cosas. Gran parte de su lógica depende del tipo de obra representada, así que tiene que preguntarle a Play por su tipo y hacer cálculos basados en eso. Esto nos remite a la última regla que nos pide no exponer getters, setters o propiedades públicas de los objetos que, a su vez, se basa en la aplicación del principio “Tell, don’t ask”. En pocas palabras: si tienes que preguntar un objeto por una información, para que actúe con base en esa información, entonces haz que el objeto se encargue de hacerlo.
De hecho, si la lógica estuviese en Play podríamos reducir el tamaño de la clase Performance. Vamos a empezar por ahí.
Fundamentalmente, podemos mover algunos métodos de Performance a Play, así que simplemente los copio y los adapto. Cuando los tenga listos, podré reemplazarlos. Voy con los relacionados con el tipo Comedy. Un detalle importante es que ahora tenemos que pasar el argumento de audiencia para permitir el cálculo.
Ahora puedo introducirlos en lugar de los existentes, que puedo eliminar a continuación, una vez que he comprobado que los tests siguen pasando igualmente.
Y pasará lo mismo con las obras de tipo Tragedy, moviendo los métodos relacionados y reemplazando las llamadas. Quedará así:
Y reduciremos el tamaño de Performance porque nos libramos de bastantes métodos.
De hecho, todavía podemos quitar un poco más de código a Performance puesto que tenemos un cálculo de créditos que depende de la obra:
Con lo que Performance se reduce hasta la mitad de líneas:
Por supuesto puedes argumentar: pero si has movido el código de una clase a otra. Ahora Play es mucho más grande. Y es cierto, pero ahora contiene casi toda la lógica que le pertenece.
Un método largo
Con todo, el método amount sigue teniendo más de cinco líneas. Hemos adelgazado la clase, pero no el método más grande. Podemos mover parte de este código a Play. Aquí tenemos un pequeño obstáculo, pues implementamos la memoización de una forma que nos complica un poco. Pero podemos arreglarlo. El primer paso es separar la memoización del cálculo:
Gracias a este cambio, además resulta que reducimos el tamaño del método amount, y el nuevo método también cumple la limitación a un máximo de cinco líneas. De hecho, ahora amount se encarga básicamente de la memoización y Play del cálculo. Más interesante aún es que se ha reducido el acoplamiento. Play no sabe nada de Performance, pero lo mejor es que esta no sabe nada de Play. Es decir: únicamente sabe que le puede pedir amount y credits, pero no tiene que saber cómo se hace el cálculo.
Este nuevo método es que queremos trasladar a Play, que sigue estando dentro del límite de tamaño.
Por supuesto, ahora queda más claro que nunca que Play necesita especializarse en dos clases Tragedy y Comedy. Pero no vamos a abordar ese cambio ahora, sino cuando la última regla nos lo pida.
Solo dos argumentos
La función statement tiene varios problemas relacionados con esta regla. Claramente, tiene más de cinco líneas en el cuerpo, incluso sin contar las inner functions. Además, una de estas funciones recibe más de dos parámetros. Vamos a ver algunas soluciones. Aquí está:
Recuerda que extrajimos esta función porque necesitábamos que alguien pudiese manejar Amount debido a la regla de no más de un punto por línea. Esto nos impediría usar la solución más inmediata que sería pasar el objeto Performance. Pero de hacerlo así volveríamos a romper la regla anterior. Por supuesto, hay más problemas ahí, pero de momento consideremos otras opciones.
Cuando una función recibe muchos parámetros una posibilidad es introducir un Objeto parámetro. Los constructores de los objetos no están limitados por esta regla, así que podríamos introducir algo como esto:
Y cambiar la función formatted_line para usarlo:
Y se podría usar así:
Pero es que, además, ahora tendría todo el sentido mover esa función a Line.
Este cambio genera algún problema porque duplicamos el código que da formato a Amount introduciendo el riesgo de que ocurran divergencias. Una forma de resolverlo podría ser introducir un patrón decorador:
De modo que se pueda usar cuando sea necesario, haciendo un par de pequeños cambios:
Más oportunidades de acortar métodos
La función statement sigue siendo demasiado larga. Por supuesto, en ocasiones nos encontraremos con que es muy difícil o imposible hacer un método más pequeño por lo que se trata de no obsesionarse. Recordemos que estamos haciendo un ejercicio para entrenar nuestra capacidad de descubrir oportunidades para aplicar las reglas. ¿Tenemos algún punto más que podamos reducir?
Parte del problema con statement es que es una función y tiene un par de líneas de inicialización de objetos. Además, al ser una función nos complica la extracción de bloques de código. Por ejemplo, el bucle que procesa las Performance podría extraerse para mantener un único nivel de abstracción. Quizá podríamos introducir el concepto de StatementPrinter para llevarnos toda esa lógica de ahí y tener más libertad para manipularla.
De este modo, statement simplemente actúa como una especie de caso de uso:
Esto nos da algunas opciones. Por ejemplo:
Una cuestión es que Printer ahora se refiere a un mecanismo concreto de impresión, así que es mejor cambiarlo de nombre y ubicación. Por otro lado, StatementPrinter,Line o FormattedAmount son objetos que hemos introducido aunque aún no hemos ubicado correctamente.
Por qué funciona
La razón de que esta regla funcione es que al querer reducir el número de líneas que contiene una clase o un método nos obliga a buscar líneas de código muy relacionadas entre sí, o sea que mantengan alta cohesión, y que puedan moverse juntas a un nuevo método o incluso a otra clase. A un nuevo método si contribuyen a la misma responsabilidad de la clase, y a otra clase si representan una responsabilidad ajena.
Cuando separamos un gran bloque de código de una clase en métodos más pequeños altamente cohesivos es fácil identificar responsabilidades, de modo que podemos analizar si realmente corresponden a la clase o deberían irse a otro lugar. Estos métodos y clases más pequeños son más fáciles de testear porque tienden a hacer una sola cosa. También son más fáciles de mantener por su pequeño tamaño, ya que podemos entender de un vistazo su propósito y si algo va mal con ellos.
Por supuesto, no siempre es posible forzar un método a tener un determinado tamaño, incluso cuando tiene una responsabilidad bien definida y sus líneas tienen mucha cohesión. En cualquier caso, siempre es buena idea intentar analizar los métodos largos en busca de oportunidades de hacerlos más pequeños.
Otra regla que se presta a mucha discusión es esta y puede ser considerada un auténtico tour de force, porque ¿qué entidad de negocio no necesita una buena cantidad de propiedades? ¿Y pretendes que únicamente sean dos?
De nuevo, una regla de calisthenics nos propone una restricción especialmente artificial que nos obliga a reflexionar sobre nuestro diseño y cómo podríamos mejorarlo. Un artículo del blog planteaba un ejercicio en el que se mostraba un ejemplo de cómo hacerlo en un tipo de datos bastante comunes en muchos negocios.
De hecho, en el ejemplo de las obras teatrales no tenemos más que un par de casos discutibles. Esto es debido en parte a lo reducido del problema, pero también porque hemos ido extrayendo todo el conocimiento a objetos pequeños.
El caso de Performance
La clase Performance contiene tres propiedades o variables de instancia:
Lo que nos encontramos en Performance es que las variables de instancia son, por decirlo así, irreconciliables. Representan cosas completamente diferentes. De hecho _amount tiene un significado puramente técnico, siendo una variable que usamos para poder realizar una optimización por lo que podríamos decir que Performance solo tiene dos propiedades: _audience y _play.
Precisamente, Play también tiene dos propiedades, aunque en este momento únicamente muestra una:
Esto es consecuencia de que simplemente hemos encapsulado una estructura de datos nativa, pero no significa que Play tenga una única propiedad. Sus dos propiedades se manifiestan en dos métodos getter, de los que tendremos que hablar en el siguiente capítulo.
Vamos a refactorizar eso:
Volvamos por un momento a Performance. Una consecuencia interesante de nuestro diseño es que el resto del programa no necesita saber de la existencia de Play, ya que todo el comportamiento de statement ocurre a través de Performance. Desde este punto de vista, Play sería irrelevante y podríamos haberla fusionado con Performance. De este modo, Performance podría tener este aspecto:
¿Recuerdas cuando Performance era demasiado grande porque se ocupaba de responsabilidades de Play? En aquel momento hubiésemos podido prescindir del objeto Play que entonces no era más que una simple Data Class (un objeto que solo tiene datos pero no comportamiento) y podríamos haber fusionado sus propiedades con las de Performance.
Esencialmente, lo que quiero decir es que cuando una clase tiene muchas propiedades, es muy probable que esté tratando de ocuparse de demasiadas responsabilidades. Si agrupamos propiedades cohesivas y extraemos nuevas clases a partir de ellas, lo más seguro es que se llevarán consigo comportamientos de la clase contenedora.
Más pequeño y más simple.
El caso de Line
Otra clase con más de dos propiedades es Line:
Line tiene tres propiedades por una buena razón, su tarea es algo así como representar un registro que tiene tres campos. Se trata de un ejemplo bastante claro de no poder reducir el número de variables por debajo del límite marcado.
Pero, ¿acaso Line no es la versión impresa de Performance? A lo mejor no necesitamos pasar las tres propiedades separadas, sino que Performance ya las agrupa. Line es como un decorador.
Y la usaríamos así:
Este enfoque es interesante. Nos permite cumplir la regla de las dos variables de instancia reemplazando Line que tiene tres por FormattedPerformance que solo tiene una.
Pero todavía nos queda una regla que aplicar y va a poner en cuestión muchas de estas decisiones.
Por qué funciona
Tanto esta como la regla anterior ponen énfasis en que las clases se ocupen de pocas cosas a la vez. Cuantas menos mejor. Para lograr eso nos fuerza a intentar cumplir con unos límites totalmente arbitrarios, que nos obligan a pensar en la cohesión de nuestro código.
La cohesión es el grado en que cada línea de código se relaciona con las demás dentro de su misma unidad (blqque, método, clase…). Cuando la cohesión es máxima, todas las líneas de código tienen que estar ahí, ninguna sobra. Para que esto ocurra, los bloques de código tienen que ser pequeños, minimizando la posibilidad de una parte del código realmente no esté contribuyendo a las responsabilidades de esa unidad.
Con las propiedades (o variables de instancia) ocurre lo mismo. Cuantas más haya en una clase, más probable es que exista una falta de cohesión. En algunos casos, el problema vendrá dado porque esas propiedades no corresponden realmente a esa clase. En otros casos, lo que ocurre es que algunas de esas propiedades son altamente cohesivas entre ellas, indicando que pueden agruparse en un objeto que represente un concepto al que contribuyen y que podemos extraer.
El objetivo de la regla es evitar que te bases en tu conocimiento del estado de los objetos de forma que acoples el resto del código a ese estado. En su lugar, los objetos solo deberían exponer comportamiento, minimizando la posibilidad de acoplarse a detalles de implementación. Por lo general, intentamos aplicar un principio llamado Tell, don’t ask, de modo que en lugar de preguntar a un objeto sobre su estado (ask), le pedimos que haga cosas.
En nuestro ejemplo hay varios casos de estos. Vamos a verlos y plantear posibles soluciones.
El caso de Invoice y StatementPrinter
En este código podemos ver que StatementPrinter le pregunta un montón de cosas a Invoice. Podríamos decir que el comportamiento de Invoice parece ser darle información sobre su estado a StatementPrinter.
De hecho, StatementPrinter sabe muchas cosas de Invoice. Por ejemplo, sabe que Invoice tiene Customer,Amount,Credits e incluso Performances. Literalmente, conoce su estructura interna.
Para intentar aligerar ese conocimiento voy a empezar a separar cosas. Haré un ejemplo paso a paso con Customer. Lo primero es extraer una variable customer para no usar directamente la invocación a Invoice.customer(). Lo que quiero es que haya un método en StatementPrinter que pueda imprimir la línea del cliente sin saber nada directamente de Invoice.
Ahora extraigo el método:
Y me deshago de la variable temporal:
Hago lo mismo con las demás datos:
También modifico el método print_lines para mantener el paralelismo:
Es cierto que seguimos haciendo llamadas de tipo getter a Invoice, pero esto nos prepara para los siguientes pasos. Queremos no preguntarle cosas a Invoice. En su lugar, Invoice podría darle a StatementPrinter la información, sin desvelar sus detalles. Para ello usaremos un patrón Visitor.
Así que en lugar de preguntarle a Invoice por su información, esta rellena los datos que StatementPrinter necesita.
De esta manera:
Este cambio aún está incompleto porque todavía StatementPrinter sigue preguntando a Performance.
En parte tendríamos que deshacer lo que hicimos al aplicar reglas anteriores porque no queremos que StatementPrinter sepa ningún detalle. Así que vamos a reintroducir un método que imprima una línea de detalles de la performance:
De este modo, Invoice puede controlar el modo que se rellena StatementPrinter, que ya no necesita saber ni siquiera cuantas líneas necesitará imprimir, pues de eso se encargará Invoice.
Así es como queda StatementPrinter:
Y así queda Invoice:
Algunos comentarios sobre lo que acabamos de hacer:
Ahora tenemos que los métodos de Invoice solo son llamados por Invoice, así que los podríamos marcar como privados. En Python podemos hacer esto prefijando sus nombres.
Una pregunta legítima que podemos hacer es si Invoice ahora sabe demasiado de StatementPrinter dato que hay cuatro métodos que tiene que conocer para poder usarlo.
Para este caso específico podemos plantear esta solución. Al fin y al cabo, lo que hacemos con StatementPrinter es rellenar una plantilla. Podríamos tener entonces un método fill más genérico en el que indicamos que plantilla queremos rellenar. Algo similar a lo que se muestra a continuación. Invoice solo tiene que conocer un método:
Y StatementPrinter ya no tiene que exponer detalles tampoco:
¿Y qué pasa con Performance? Sigue exponiendo getters. Así que podríamos hacer algo similar:
Y ahora Invoice no tiene más que decirle a Performance que rellene su parte:
A continuación, lo suyo sería hacer privados todos estos getters o incluso eliminarlos.
El patrón de relación que nos ha quedado entre Invoice y StatementPrinter se llama Double Dispatch, pero podemos simplificar un poco las cosas de esta manera. StatementPrinter ya no sabe nada de Invoice:
Y la función statement queda así, and I think it’s beautiful:
El caso especial de Play
El problema con Play está aquí:
Play tiene que preguntarse “¿qué tipo de obra soy?”, para decidir como realizar el cálculo que le piden. Esto es muy similar a una violación del principio Tell, don’t ask, ya que tiene que consultar una propiedad para poder escoger el algoritmo adecuado.
Los objetos tienen propiedades por algo. Normalmente, la razón de ser de esas propiedades es ser capaces de regular el comportamiento del objeto. Las propiedades tienen un papel similar al de los coeficientes de una ecuación y operan junto con los parámetros que se pasan a los métodos para calcular un resultado.
Sin embargo, propiedades que modelan el tipo de un objeto son harina de otro costal. Aportan el criterio para decidir qué algoritmo utilizar al realizar el cálculo. Pero si un objeto es de un tipo, esto debería reflejarse en el código por su clase. Cuando un objeto de una clase tiene tipo, y ese tipo determina la forma en que efectúa su comportamiento, lo que ocurre es que la clase debería tener variantes especializadas basadas en su tipo, ejecutando su comportamiento en su forma particular.
En nuestro ejemplo, está muy claro que hay dos tipos de obras: comedias y tragedias. Ambos tipos son obras teatrales, pero para los efectos de nuestro ejemplo, calculan sus importes y sus créditos de forma diferente.
¿Cómo podemos refactorizar Play para extraer las dos subclases? Vamos a ver un procedimiento bastante mecánico. En primer lugar duplicamos Play para crear la clase Tragedy, que extenderá de la misma Play:
El siguiente paso es reemplazar todas las condicionales sobre self.type() por True. En nuestro ejemplo, solo tenemos un caso en el método amount:
Probablemente, el IDE habrá empezado a señalar que la condicional es redundante porque ahora siempre se cumple. En mi caso está señalando que el resto del código del método no se ejecutará nunca. Así que podemos borrarlo:
De hecho, nos sobra la condición:
Al hacer esto, dejamos de llamar a varios métodos, los que ejecutaríamos si el tipo fuese comedy. También los borramos porque no se llaman en más sitios. Nos va quedando esto:
El siguiente paso es cambiar todas las condicionales que buscan tipos que no sean “tragedy” para reemplazarlas por False. En Tragedy ya no se da ese caso. Sin embargo, en credits tenemos una condición inversa que en el contexto de Tragedy equivale a comprobar si el tipo es tragedy. Así que en realidad, la condición siempre se cumplirá:
Todo el código fuera de la condición no se ejecuta y lo borramos, por lo que el resultante será:
El método amount llama sin más a otro método, así que podríamos integrar este último, así como eliminar referencias superfluas en el nombre del método que nos dice el importe extra. Tragedy quedará así y podremos eliminar también la propiedad type y todo lo relacionado con ella:
Aplicamos el mismo tratamiento a Comedy. Empezamos duplicando Play y reemplazando todas las condicionales que verifican el tipo de tal modo que aquellas que chequean que el tipo es comedy sean siempre True y las que no siempre False:
A continuación, eliminaríamos todo el código muerto y que no se ejecuta porque ya no será llamado nunca.
Y rematamos integrando y cambiando nombres:
Ahora vamos a ver como utilizar las nuevas clases especializadas. El lugar en el que se instancian objetos Play es aquí:
Una forma sencilla sería introducir un método factoría en Play que nos entregue la subclase adecuada:
Y usarla:
Ahora, no queda más que eliminar todos los métodos y propiedades innecesarias en Play:
El método factoría create decide qué subtipo concreto de Play se usará. Si en el futuro necesitamos dar soporte a más tipos no tenemos más que añadir una nueva clase y una nueva condición.
Por qué funciona
Esta regla suele ser más difícil de aceptar o entender si vienes de un estilo de programación procedural en el que conocer y controlar el estado lo es todo. Pero en programación orientada a objetos, cada objeto es responsable de su propio estado y de como implementa sus comportamientos. Por tanto, lo más importante es saber quién debe encargarse de qué, en lugar de tratar de obtener su estado y operar con él.
Cada objeto debe operar con su estado y comunicarse con otros objetos cuando necesite algo, o cuando quiera enviarles algo.
Al preguntar por una propiedad de otro objeto estamos acoplándonos a ese objeto, porque sabemos qué propiedad nos interesa y cómo obtenerla. Si usamos ese dato para un cálculo, es muy posible que ese objeto al que le preguntamos deba ejecutar ese cálculo. Por supuesto, puede ocurrir que el cálculo requiera alguna información del objeto que llama. Pero en ese caso la puede pasar como parámetro.
La regla de no usar getters, setters o propiedades públicas, nos fuerza a pensar en los objetos como cajas negras a las que podemos pedirles que hagan cosas. En pocas palabras, la regla nos dice que no debemos acceder al estado interno los objetos del sistema. Si necesitamos algo de ellos, tenemos que poder pedirles que lo hagan, aportando información si es necesario. Algunos lenguajes como Ruby fuerzan que todas las propiedades de un objeto sean privadas por defecto, aunque es fácil introducir getters o setters.
El hecho de no poder acceder al estado de los objetos es beneficioso para evitar el acoplamiento. Nos permite cambiar las implementaciones de los objetos de forma transparente al resto del sistema.
Este capítulo nos ha presentado la última regla de las Object Calisthenics. Pero todavía nos queda un capítulo más, en el que daremos algunos consejos y exploraremos algunas ideas más.
Comentarios finales
Aplicar object calisthenics, ¿mejora el diseño?
Sí, aplicando las reglas de object calisthenics el diseño del código mejora aunque no lleguemos a introducir patrones de diseño avanzados. En otras palabras, calisthenics te ayuda incluso si no tienes mucha experiencia en diseño de software.
En líneas generales, reducir el tamaño de los bloques de código y aplanar las estructuras indentadas ayuda a tener bloques y métodos más cohesivos centrados en torno a una sola responsabilidad bien acotada.
Encapsular primitivos y estructuras de datos nativas abre la puerta a una mejor asignación de las responsabilidades, moviendo comportamientos a los objetos a los que corresponden.
¿Hay un orden adecuado para aplicar las reglas?
No. Las reglas se aplican según lo necesitamos o nos parece más evidente que se pueden aplicar. Muchas veces, aplicar una regla genera situaciones que se abordan aplicando otra. Así que en realidad, lo que hacemos es observar fragmentos de código que violan una u otra regla y los arreglamos lo mejor posible.
El proceso es, por tanto, iterativo. Empiezas aplicando una regla cuya utilidad ves clara y vas haciendo pequeños commits con los cambios que ves que mejoran tu código. En algún momento, descubrirás oportunidades para aplicar otras reglas u otros refactorings y así sucesivamente.
¿Por dónde empezar?
Empieza aplicando la regla que te resulte más fácil o cuyos casos sean más evidentes. Por ejemplo, no usar abreviaturas es fácil de aplicar en casi cualquier código. Aplanar estructuras condicionales suele ser muy evidente y el refactor extraer método es sencillo de aplicar en un IDE moderno.
Encapsular tipos primitivos y estructuras de datos no es difícil, pero ya supone un trabajo extra porque tenemos que asegurar que en todos sus usos podemos hacer la sustitución. Sin embargo, una vez introducido un concepto como objeto, mover comportamiento viene de forma casi natural. En muchos caos puede ser la mejor regla para empezar, ya que luego tienes que estar menos pendiente si extraes métodos.
Eliminar la palabra clave else puede ser complicado si no aislamos las estructuras condicionales previamente, para lo cual es bueno haber aplicado antes la regla de un solo nivel de indentación.
No usar getters o setters puede ser muy sencillo en algunos casos, pero no ser evidente como hacerlo en otros. En uno de los ejemplos de estos artículos, introdujimos el patrón Visitor para hacerlo, pero no es uno de los más sencillos de aplicar precisamente.
¿Debo aplicar las reglas exhaustivamente en todo el código?
No. Céntrate sobre todo en la lógica de dominio, que es la que más te interesa que sea fácil de entender y de mantener en el futuro. Las mejoras del código en esta área son más prioritarias, porque los objetos tienen mayor significación. En las partes de implementación de infraestructura, los beneficios pueden no ser tan importantes, lo que no debería justificar un diseño chapucero.
Usa tu buen juicio. Céntrate en el código que sea importante.
Más consideraciones y ejemplos sobre algunas reglas
Más sobre encapsular primitivas
Me he dejado algunos valores primitivos sin encapsular. El criterio de prioridad para encapsular primitivas sería algo así como: Encapsula primitivos en objetos cuando:
El primitivo representa un concepto relevante del dominio o negocio de la aplicación.
El primitivo tiene reglas validación o comportamiento asociado que no es soportado por el propio tipo, lo que básicamente indica que el concepto es importante para el dominio.
Por ejemplo, tras aplicar la última regla a Play y extender en dos subclases, quedó de manifiesto que el cálculo de importe extra en relación con la audiencia era un comportamiento asociado al concepto de Audiencia. De hecho, el IDE señala esos métodos como candidatos a ser métodos estáticos. Por ejemplo, en Tragedy es así:
Y en Comedy, así:
Como se puede ver ninguno de los dos métodos depende de la clase que los contiene. Es cierto que podríamos extraer sus valores como propiedades de su clase. Sin embargo, fíjate que todo el cálculo se refiere solo al concepto de Audience. Hay un límite por encima del cual se genera un Amount extra. Si no se supera el límite Amount es cero.
Si igualamos la estructura de los métodos para que se parezcan lo más posible, quedaría una cosa así. Para Tragedy:
Y para Comedy:
Podríamos introducir una clase Audience que nos calcule el Amount extra, pasándole los parámetros necesarios:
Y podemos usarlo:
O más simplificado:
Ahora tendría sentido introducir las propiedades de Tragedy que representan los parámetros:
Nos quedaría código relacionado con Audience y la posibilidad de instanciar el objeto desde el principio. Personalmente, cuando se trata de refactors suele empezar a introducirlo lo más adentro y voy “sacando” el objeto un paso cada vez. Así, por ejemplo, en el caso de credits, la lógica tiene que ver con Audience, pero no está tan claro como aplicar la relación.
Más sobre límites de tamaño: parámetros y propiedades
Introducir Audience ha generado un problema, ya que la función para calcular el extra requiere tres parámetros y además hemos introducido tres propiedades más en las clases Play, ni más ni menos. En este caso, puede ser de aplicación el patrón Parameter Object para agruparlos. Sería algo así como ExtraAmountData:
Esto se tendría que aplicar más o menos así. En Audience:
Pero esto, sin embargo, no pinta bien. Audience no debería ser la responsable de calcular el extra, sino que es un dato necesario para hacerlo. Tendría más sentido que otro objeto dirija el cálculo sin exponer todos sus datos. Se podría considerar una especie de calculadora del importe extra basada en la audiencia, con coeficientes definidos por cada tipo de obra. Así que vamos a cambiar el concepto por completo.
Y esto se usaría así:
Los tres parámetros de ExtraAmountByAudience son bastante crípticos. Una posible solución es usar un patrón builder:
Con lo cual, podemos hacer una construcción más expresiva:
¿Sobre-ingeniería?
Comentarios
El objetivo de este libro no era tanto llegar a un diseño de código final, como mostrar que aplicando las reglas de Calisthenics es posible mejorar el diseño del software a través de dos caminos. El más simple consiste en aplicar las reglas tal cual. El segundo consiste en avanzar a partir de ese punto, descubriendo oportunidades para aplicar patrones de refactoring más avanzados.
Acoplamiento temporal al imprimir la factura
Cuando se publicó este proyecto en el blog, Josemi, un lector, señaló un caso de acoplamiento temporal dado que StatementPrinter no controla el orden en que se imprimen los elementos del Statement. Esto es debido a que no hay una separación entre la obtención de los datos y su impresión. El método fill obtiene el dato e imprime la línea. De ese modo, el control lo tiene Invoice, así que bastaría cambiar el orden de las llamadas en Invoice para romper la impresión del Statement.
Esta es una primera aproximación muy basta, pero suficiente para hacernos a la idea y que elimina el acoplamiento temporal:
Ahora podría cambiar el orden de las líneas en Invoice, sin afectar al resultado:
Más sobre colecciones de primera clase
Otra sugerencia de Josemi es que la clase Performances, que contiene la colección de actuaciones se encargue también de controlar el orden en que se envían las líneas a StatementPrinter, en lugar de Invoice. Me parece una propuesta interesante. Sería una aplicación del principio Tell, don’t ask. Invoice le pide a Performances que realice la coordinación y cálculos que ahora mismo se hacen en Invoice, que quedaría así:
Mientras que Performances podría quedar así, una vez eliminado el código para hacerla iterable que ya no es necesario:
Esta vez, sí. Lo cierto es que probablemente todavía podríamos introducir mejoras en el código. Hay algunas clases que no me convencen del todo, como ExtraAmountByAudience o StatementPrinter. Seguramente esconden aún problemas en el diseño que no he sido capaz de ver.
Leanpub requires cookies in order to provide you the best experience.
Dismiss