- Diseño de las pruebas
- Implementación
- Refactoring
- Conclusiones y código
Refactoring
En ocasiones cuando un proyecto va creciendo y va adquiriendo más funcionalidad y responsabilidades, sin darnos cuenta podríamos estar repitiendo líneas de código en distintas partes del proyecto y por ende, dar mantenimiento a un proyecto donde se repita el código en distintas partes puede resultar una tarea engorrosa, difícil y propensa a errores, ya que cada vez que exista un cambio, se realiza normalmente de forma manual, y si olvidamos actualizar el código en algún lugar donde sea necesario, seguramente habrá problemas a la hora de correr nuestras pruebas. Por ejemplo, en la clase SimpleBinaryTree existía un error en la implementación del método de borrado, misma que detecté en una de las pruebas cuando copié el código para reutilizarlo en la clase AVLTree. Cuando arreglé el error, únicamente lo hice dentro da la clase AVLTree, de modo que el código del borrado en la clase SimpleBinaryTree, seguía siendo incorrecto.
Las líneas repetidas podrían concentrarse en un solo lugar y de ahí ser llamadas por aquéllos miembros del proyecto que están ocupándolas, haciéndolo más accesible para su mantenimiento sin afectar su correcto funcionamiento. El proceso de identificar código repetido que puede ser reusado y colocarlo en un solo lugar para hacerlo disponible a los métodos o clases que lo estén utilizando es parte de las técnicas de Refactoring o Code Refactoring.
Refactoring de hecho es parte del proceso de Desarrollo de Software dirigido por Pruebas. Esta es la razón por la cual decidí hablar de él en esta tercera parte de la serie. Las clases SimpleBinaryTree y AVLTree del proyecto Trees con el cual he venido ejemplificando este proceso, se encuentran en un punto en el cual, a pesar de funcionar correctamente, (es decir, todas las pruebas que diseñamos previamente a su implementación corren exitosamente) ambas clases contienen exactamente el mismo código para insertar y borrar un nodo dentro de un árbol. La única diferencia es la implementación de rotaciones en la clase AVLTree que permiten el balanceo del árbol.
Por lo tanto, existe código repetido en ambas clases y el siguiente paso es, precisamente la realización del refactoring que permitirá reducir visiblemente el número de líneas de código en dichas clases sin afectar su funcionamiento. La manera de demostrar esto último será al volver a correr las pruebas con las que ya contábamos y que corrían exitosamente antes del proceso de refactoring: si las pruebas vuelven a correr exitosamente, habremos realizado el refactoring de manera apropiada.
De manera similar, los procesos de rotación que se llevan a cabo en la clase AVLTree, se reutilizarán en la implementación de la clase RedBlackTree, por lo tanto, podemos realizar también el refactoring para los métodos de rotación que se encuentran actualmente en la clase AVLTree.
En la siguiente imagen se aprecian los métodos de la clase AVLTree antes de realizar el refactoring y los resultados de las pruebas desarrolladas tanto para esta clase como para SimpleBinaryTree. Como pueden observar, ambas clases funcionan correctamente.
Ahora bien, podríamos simplemente mover el código referente a la inserción, al borrado y a las rotaciones a la clase abstracta BinaryTree, la cual es heredada por todas las demás, sin embargo, esto no es lo más correcto, ya que las rotaciones no son necesarias en la clase SimpleBinaryTree. Para evitar este problema, creé la clase abstracta BalancedBinaryTree, la cual también hereda a BinaryTree pero se encarga únicamente de las rotaciones. De este modo, BalancedBinaryTree adquiere toda la funcionalidad de BinaryTree, pero únicamente las clases que la hereden podrán adquirir la funcionalidad de las rotaciones y en este caso, las clases que la heredarán serán AVLTree y RedBlackTree. La inserción básica y el borrado básico pueden permanecer en BinaryTree ya que todas las clases que hereden a BinaryTree deben contar con métodos de inserción y borrado de nodos.
La nueva relación existente entre las clases queda ilustrada a través de este diagrama:
Después de haber realizado todos estos cambios, volvemos a ejecutar las pruebas y observamos los resultados. En la imagen de abajo se puede observar cómo se redujo de manera dramática el cuerpo de la clase AVLTree después de haber realizado el refactoring, pero lo más importante es que las pruebas tanto para AVLTree como para SimpleBinaryTree siguen siendo exitosas. Esto quiere decir que a pesar de todos los cambios que realizamos, el proyecto sigue funcionando correctamente y esta es una de las grandes ventajas de las pruebas: podemos asegurarnos de que nuestro programa sigue funcionando después de haber realizado varios cambios con tan sólo volver a correr las pruebas que diseñamos al principio y verificar que éstas siguen siendo exitosas. Si ocurre algún error, los mensajes que se despliegan en la ventana de Test Results son de gran ayuda para localizar las líneas de código donde se está presentando el problema.
Todo lo que hasta ahora hemos realizado forma parte del ciclo de Desarrollo de Software Dirigido por Pruebas: hemos creado las pruebas, las corrimos para observar que éstas fallan pues en ese momento no existía la implementación de los métodos que estamos probando, después realizamos la implementación de dichos métodos, acto seguido volvimos a correr las pruebas y cuando éstas fueron todas exitosas, concentramos nuestra atención en el proceso de refactoring a modo de mejorar la estructura del proyecto y de hacerlo más fácil de mantener, el último paso consiste en la repetición de cada una de estas etapas cada vez que se quiere agregar funcionalidad al proyecto.
En los próximos posts, hablaré de la implementación de la clase RedBlackTree, que es la última clase del proyecto que hemos estado analizando, por lo que estaré llevando a cabo el último paso del ciclo mencionado: la repetición de todos los pasos comenzando siempre por la elaboración de las pruebas.
No hay comentarios:
Publicar un comentario