¿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 |
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:
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
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: