Me gusta el reto de llegar a un proyecto ajeno y domarlo. Entender como lo diseñaron y ver como mejorarlo. Pero antes de poder ampliar ese código hay que estabilizarlo para poder caminar seguros. El objetivo con el que atacamos ese código es poder tener una base de código entendible y con cobertura de tests que nos dé tranquilidad.
El enfoque es gradual y seguro, priorizando la comprensión del código, las pruebas y la refactorización incremental. Haremos primero los pasos 0, 1 y 2 y luego realizaremos los pasos 3 por bloques de funcionalidad.
Paso 0: Analizar el código
Antes de meterle mano al proyecto, tendremos que entender como se ha organizado el código y que patrones se han seguido. Queremos entender que decisiones se tomaron (nomenclaturas, ) así como identificar dependencias, funciones críticas y áreas problemáticas. Así como archivos grandes y lógica spaghetti.
Aquí habría que documentar hallazgos y anotar prioridades. Y dejaremos abierto un documento con la deuda técnica que nos iremos encontrando más adelante.
Paso 1: Control de versiones
El código al que hemos entrado debería estar gestionado por un control de versiones (aquí Git es mi amigo). Es una manera de controlar nuestros cambios y poder echar atrás si vemos que el camino tomado es incorrecto.
Paso 2: Poner en marcha el proyecto en local
Aquí un sistema de contenedores es nuestro amigo. Si no lo tiene configurado, creo los contenedores Docker con Compose que necesite para replicar el entorno final, sin importar la máquina que tengo.
Pueden ser varios contenedores para cada uno de los servicios y uso nginx-proxy para servir el proyecto con una url local.
Además dejaremos listo el sistema de tests. Las pruebas actúan como red de seguridad para lo que viene a continuación. En este punto podremos repartir el proyecto al equipo.
Paso 3: Refactor + Tests
Aún no vamos a escribir código nuevo, pero si vamos a empezar a moverlo. Iremos eligiendo partes del código en base a su criticidad (o necesidades del proyecto) y las iremos recorriendo paulatinamente.
Paso 3.1: Characterization test
Añadiremos characterization tests (test de comportamiento) para cubrir la funcionalidad existente sin romperla. Serán tests sencillos que nos darán la tranquilidad de fallar cuando rompamos el código.
Paso 3.2: Extraer métodos / funciones
Empezaremos haciendo refactorizaciones pequeñas como «Extract Method» para dividir funciones grandes. Vamos a buscar piezas que tengan funciones con demasiadas funcionalidades y vamos a simplificar el código creando funciones pequeñas que hagan una cosa concreta. Además les daremos nombres que sean fáciles de comprender (nómbrala con un verbo).
También detectaremos cierto código repetido que se puede llevar a helpers.
Cuidado, no debemos crear código, solo refactorizar para crear código mas sencillo, comprensible y que sea fácilmente testeable. Tu IDE te ayudará a hacer esto de manera rápida y eficiente.
Aunque es tentador ponernos a reescribir código, tendremos que dejarlo anotado en el documento de deuda técnica y seguir adelante, esto aún no es terreno estable.
Paso 3.2: Constantes
Al igual que tener funciones que tengan un nombre claro, no me gusta encontrarme «números/strings mágicos» en el código. Aquí nos llevaremos todos estos valores a constantes que podrán ser definidas en un scope local de la función o a nivel de aplicación, pero siempre con un nombre que nos ayude a entender su utilidad.
Algo muy habitual que hago es crear enums en los modelos con los posibles valores para un campo. A veces uso contantes dentro de funciones por la simple tranquilidad de que tengan un nombre, quizá más adelante descubra que se usa el mismo valor en varios sitios y pueda usar la misma constante sacándola a un scope más amplio.
Paso 3.3: Datos de prueba
Como ya conocemos la estructura de datos mínima para levantar el proyecto, ahora vamos a automatizar esto. Crearemos ficheros de seeders y factories.
Paso 3.4: Tests unitarios
Sobre las funciones que estamos extrayendo (en especial con las que van a helpers) prepararemos una batería de test que nos de tranquilidad de su comportamiento.
Paso 4 (opcional): ¿Actualizamos versiones?
Ahora podemos atrevernos a actualizar versiones del entorno (lenguaje, base datos), de librerías externas… para dejarlo todo con las versiones que estén en mantenimiento en ese momento y así evitar errores ya corregidos y brechas de seguridad.
Y ya tenemos el código listo
A partir de este punto, ya podríamos empezar a evolucionar el código con un margen de confianza bastante alto. Ya podremos empezar a atajar la deuda técnica que hemos encontrado, y podríamos plantearnos modernizar el código a nuevos patrones de diseño, crear inyección de dependencias para desacoplar el código, o incorporar feature flags para ir cambiando el flujo del código antiguo al nuevo… El proyecto ya está domado y podemos evolucionarlo.
Bola Extra: domando y refactorizando en tiempos de IA
Al momento de escribir estas líneas no tendría confianza en dejarle a solas a la IA un proyecto legacy, ya que podría romperlo por demasiados sitios y sería imposible de manejar PERO una vez refactorizado el código con funciones y constantes con nombres claros, cualquier agente IA puede entender mejor el código y ayuda a generar tests para este. Un agente IA a modo de copiloto siempre será de buena ayuda.