Inicio ::: Blog ::: Septiembre 2020

Mejorando TDD con la premisa de la prioridad de transformación

#softwaredevelopment #testing #TDD #TPP #agil #buenaspracticas

Publicado el 17/09/2020 por Alexander Castañeda

¿Qué es la Premisa de la Prioridad de Transformación?

La mayoría de los programadores que han utilizado la práctica de desarrollo de software TDD (Rojo/Verde/Refactorización) en su ciclo de trabajo, deben estar ya acostumbrados a introducir pequeños cambios en el código en cada una de sus fases, y principalmente deben conocer los cambios asociados a las refactorizaciones que realizamos durante la última fase de TDD.

La refactorización, tal como fue definida por Martin Fowler en su libro Refactoring: Improving the Design of Existing Code (1999), “es el proceso de cambiar un sistema de software, de forma tal que no se altere el comportamiento externo del mismo pero que se mejore su estructura interna. Con el objetivo de hacerlo más sencillo de entender y barato de mantener, permitiendo que el diseño del programa mejore luego de haber sido escrito”.

Al igual que con la fase de Refactorización, durante la fase Verde también se realizan alteraciones en el código, mediante las cuales intentamos hacer que el software supere la prueba que se encuentra fallando, empleando el menor cambio posible. A estas operaciones Robert C. Martin (tío Bob) las denominó transformaciones.

Podemos pensar en estas transformaciones como la contraparte de las refactorizaciones, debido a que consisten en pequeñas variaciones en el código que permiten modificar el comportamiento externo del software pero que conservan significativamente su estructura interna. Dicho en otras palabras, las transformaciones generalizan el comportamiento del sistema sin cambiar su estructura.

Pensando en estas transformaciones, el tío Bob definió la “Premisa de Prioridad de Transformación” (sus siglas TPP del inglés Transformation Priority Premise) la cual afirma que las transformaciones poseen un orden inherente fundamental, y que al ser aplicadas en correcto orden se conduce a mejores algoritmos y se evitan interrupciones del ciclo de TDD.

Él describe esta premisa en algunas de sus charlas mediante un ejemplo de desarrollo de un algoritmo de ordenamiento, y hace notar que al violar dicho orden se conduce a un algoritmo de ordenamiento de burbuja, mientras que empleando un orden preestablecido se alcanza un algoritmo de ordenamiento rápido, el cual es mucho más eficiente que el anterior.

Basándose en sus experiencias y evaluando publicaciones de otras personas, el tío Bob llegó a la siguiente lista ordenada de transformaciones:

01 {}–>null De no tener código alguno a código que retorna Null
02 null->constant De código que retorna Null a devolver un valor literal simple
03 constant->constant+ De un valor literal simple a un valor literal complejo
04 constant->scalar De un valor literal a una variable
05 statement->statements Agregar más sentencias
06 unconditional->if Dividir el flujo de ejecución mediante condicionales
07 scalar->array De una variable a una colección
08 array->container De una colección a un contenedor
09 statement->tail-recursion De una sentencia a recursión de cola
10 if->while De un condicional a un bucle while
11 statement->non-tail-recursion De una sentencia a una iteración
12 expression->function De una expresión a una función
13 variable->assignment Reemplazar el valor de una variable
14 case/else Añadir una bifurcación a un condicional existente



Tabla 1. Orden de Transformaciones según su Prioridad

Este listado sugiere un grado de complejidad incremental en nuestras transformaciones a medida que avanzamos en él, de forma que es más sencillo cambiar una constante a una variable que el añadir una sentencia condicional. Debido a esto, se sustenta la idea de dar prioridad en su uso a las transformaciones más simples por encima de aquellas más complejas.

Como una guía para nuestro uso durante el ciclo de TDD se puede representar dicho listado con el siguiente diagrama:

Figura 2. Diagrama de la Premisa de Prioridad de las Transformaciones

Para propiciar el uso de las transformaciones más simples, cuando diseñamos una prueba, primero debemos pensar en desarrollar aquellos casos de uso que nos permitan emplear transformaciones más simples en vez de las transformaciones más complejas. Debido a que a mayor complejidad de la prueba, mayor será el riesgo que tomemos para lograr hacer que nuestra prueba pase.

Encontrarnos en una situación donde no sabemos cómo hacer que una prueba pase, es un problema que muchos hemos observado mientras realizamos TDD. Algunas veces, la causa de este tipo de impasses recae en nuestro proceso de toma de decisiones durante el ciclo de desarrollo de TDD.

La transformación más simple suele ser la mejor opción que incurrirá en el menor riesgo para crear una situación de bloqueo. Debido a que mientras mayor sea el cambio en el código, más tiempo nos tomará antes de que nuestra prueba vuelva a pasar, por lo que se produce un riesgo mayor de romper el ciclo de TDD.

Por todo lo antes expuesto, el tío Bob plantea como premisa que si se eligen las pruebas e implementaciones que empleen transformaciones que están más arriba en la lista, se podrán evitar las situaciones de impasse o interrupciones prolongadas en el ciclo rojo / verde / refactorización.

Aplicando la TPP al Kata de los Números Primos

Podemos practicar y dominar estos conceptos aplicando esta premisa de la prioridad de las transformaciones en un problema conocido como lo es la kata de los números primos.

El objetivo de esta kata es determinar el listado de factores primos un número. Un número primo sólo puede ser dividido exactamente por sí mismo y por 1.

A continuación, realizaremos la kata de los factores primos:

1. Añadimos un Test Fallido para 1 (Caso más simple)

Nos detenemos al fallar la compilación de la prueba

1.1 Transformando para Compilar

La forma más fácil de tener éxito (compilar ahora) es crear un método con valor de retorno nulo. Esta es la primera transformación o transformación nula ({} -> null).

1.1 Transformando para Pasar el Test

Esta prueba falla porque el método primeFactorsOf devuelve null, y no una lista vacía. Para que esta prueba sea exitosa, transformaremos el valor nulo en una lista vacía.

Nuestra segunda transformación es null -> constant.

¿Acaso null no es una constante? Null también es una constante, pero Null es una constante muy especial. Es una constante sin tipo definido y sin valor. Por lo tanto, es diferente de una constante que tiene un tipo y un valor.

La transformación realizada con null -> constant hace que el código sea más general. Null es un caso muy especial, inmutable, pero una lista vacía tiene el potencial de ser general. Ser general significa que puede manejar una variedad más amplia de casos.

2. Añadimos un Test Fallido para 2

Como segunda prueba, agregamos:

assertThat(primeFactorsOf(2), is(Arrays.asList(2)));

Con lo que esta nueva prueba fallará.

2.1 Transformación - Constante a Variable

Para que esta prueba sea exitosa, generalizamos la constante new ArrayList()

Para hacer esto, transformamos la constante en una variable llamada factors. La tercera transformación es constant -> variable.

Lo que hemos hecho es una refactorización (extracción de una variable), con la cual no hemos cambiado el comportamiento en un sentido estricto. Debido a que reemplazar una constante por una variable con su valor original no altera el comportamiento del sistema. Sin embargo, nos abre la posibilidad de poder cambiar ese comportamiento. En otras palabras, cambiar una constante por una variable no es una transformación independiente, pero es una parte necesaria del proceso de transformación.

2.2 Transformación - Flujo Dividido

Una transformación que modifica una constante en una variable permite que realicemos una cuarta transformación llamada división de flujo. En la transformación de flujo dividido, una sentencia if es empleada para dividir el flujo de control del programa. Esta transformación fue habilitada por la transformación anterior. La sentencia if hizo de nuestro código algo más específico. Por lo que nos toca ahora generalizarlo.

2.2 Refactorización - Generalizando

La condición if(number == 2) fue un caso muy específico (coincidiendo con la intención de la prueba). Debemos refactorizar para manejar casos más generales.

if(number > 1) es más general debido a que está abierto a posibilidades.

3. Añadimos un Test Fallido para 3

Al incluir la tercera prueba assertThat(primeFactorsOf(3), isListOf(3)); dicha prueba fallará.

3.1 Transformación - Constante a Variable

Para lograr hacer pasar la prueba con la transformación más simple, aplicamos la transformación constant -> variable en la cual modificaremos el 2 que añadimos a la lista de factores por la variable de entrada number.

4. Añadimos un Test Fallido para 4

Para la cuarta prueba incluimos la siguiente sentencia:

assertThat(primeFactorsOf(4), is(Arrays.asList(2,2)));

4.1 Transformación - Flujo Dividido

De forma que logremos pasar esta prueba, debemos volver a hacer una división en el flujo, dependiendo si la entrada number es divisible por 2.

4.2 Transformación - Flujo Dividido Nuevamente

Aún con este cambio, otra de nuestras pruebas continúa fallando. Esto se debe a que el caso de los factores primos de 4 es exitoso, pero el caso de los factores primos de 2 debe incluir a un solo valor y no dos. Por lo que tendremos que dividir el flujo nuevamente para evitarlo.

Y con esto podremos pasar todas las pruebas.

4.3 Refactorización

En el caso de number > 1, la división es realizada dos veces. No debemos preocuparnos de ello en este momento debido a que pronto desaparecerá. No generamos ningún inconveniente si movemos la parte del segundo condicional number > 1 fuera de la primera cláusula condicional.

4.4 Refactorización

Al igual que con el código productivo, el código que compone nuestras pruebas también debe ser mantenido. Por lo que podemos refactorizar para mejorar su legibilidad al eliminar duplicación en el código.

5. Añadimos más pruebas

En los casos de 5, 6 y 7, estas pruebas funcionan correctamente si son incluidas debido a que su comportamiento se encuentra incluido en nuestro algoritmo.

6. Añadimos prueba fallida para el 8

assertThat(primeFactorsOf(8), isListOf(2, 2, 2));

La cual falla.

6.1 Transformación - If a While

Podemos aplicar la transformación If -> while de forma que pasemos la nueva prueba.

El bucle while es simplemente una forma generalizada del condicional if, por lo que if es una forma especial de la estructura while.

6.2 Refactorización

Se mejora la legibilidad del código al refactorizar el bucle while a un bucle for como se muestra a continuación.

7. Añadimos un Test Fallido para 9

Añadimos la prueba assertThat(primeFactorsOf(9), isListOf(3, 3));

7.1 Hacemos pasar con Duplicación

Añadimos la condición de división por 3 como se muestra a continuación para hacer que nuestra prueba pase.

7.2 Transformación para remover la duplicidad - Bucle más general

Hemos introducido una duplicidad en nuestro código. Podemos incurrir en estas mientras estemos trabajando en un ciclo de TDD, pero las mismas no deben registrarse en el repositorio de origen como código repetido. Esta redundancia no es una transformación, debido a que nada ha sido generalizado. Simplemente es un experimento que nos permite imaginar como debe ser la solución general.

Nuestra tarea ahora es aplicar un ciclo más generalizado para eliminar la duplicación. El código duplicado siempre es inusual y específico.

7.3 Refactorización

Mejoramos nuevamente la legibilidad al refactorizar el bucle while por un bucle for y eliminamos el condicional restante, para obtener el resultado final compuesto por dos bucles for fáciles de entender.

Conclusión

Las siguientes transformaciones fueron observadas en este ejercicio de ejemplo:

  • {} -> null (1)
  • null -> constant (2)
  • constant -> variable (4)
  • split flow (6)
  • if -> while (10)

Pudimos ver mediante la kata de ejemplo una ilustración del proceso descrito por el tío Bob, y ver que al aplicar las transformaciones que propone TPP conseguimos garantizar:

  • Menor tiempo en rojo (evitar el impasse)
  • Descubrimiento de los puntos principales del problema
  • Desarrollo de una solución genérica y con menos duplicación

La Premisa de la Prioridad de Transformación (TPP) es una de las herramientas con las que contamos para garantizar el cumplimiento de las reglas del desarrollo guiado por tests. Este listado de transformaciones no implica reglas inalterables, sino una guía para evitar los bloqueos en el ciclo de TDD, especialmente al hacer el cambio de rojo a verde.

Si te interesa conocer más sobre TPP puedes buscar el siguiente material: