leanmind logo leanmind text logo

Blog

TDD Avanzado

Las herramientas que se necesitan para aplicar TDD en el mundo real en cualquier proyecto.

Cuando el legacy deja de ser código: Cómo lidiar con legacy code basado en queries 🙆‍♂️

Por Rubén Zamora y Borja García

En nuestro día a día como developers, todas/os nos encontramos o hemos encontrado con proyectos de hace unos años escritos en un código que no se concibió con la idea de ser mantenible, sobre el que nos toca trabajar aunque no nos inspire confianza para hacerlo sin romper nada.

Aunque es menos común, hay ocasiones en las que este spaghetti code está implementado en forma de sentencias SQL de 200-300 líneas, con toda la lógica de dominio incrustada en las propias consultas, sobre un modelo de datos innecesariamente enrevesado que gana complejidad con los años por desconocimiento, y por miedo a hacer modificaciones para no romper un sistema que funciona. Nos hemos encontrado este tipo de situación, por ejemplo, al trabajar en un proyecto basado en añadir personalizaciones sobre un ERP que originalmente no estaba pensado para ello.

En este artículo, propondremos una pequeña estrategia para enfrentar esa situación, en la que nos piden desarrollar una funcionalidad nueva sobre este código, sin morir en el intento :skull:.

Cómo atacar el problema

1. Estudia su comportamiento

Parece obvio, pero es muy probable que en estos casos los autores originales del código ya no se encuentren en la empresa, y nadie sepa exactamente cómo funciona, a excepción de alguna idea vaga de gente que ha trabajado en desarrollos relacionados.

Esto nos lleva a que, el primer paso sea un spike (investigación) sobre las clases que contienen las consultas, en el que tendremos que responder preguntas como:

Para este tipo de spike, viene muy bien usar herramientas como Notion para ir documentando, a la vez que generamos más conocimiento sobre el sistema.

2. Tests arnés

Aunque lo situemos como segundo paso, en realidad escribir tests a modo de arnés de seguridad también sirve para tener una imagen del comportamiento de las consultas en base a su input y output, e incluso para ponerlas a prueba con casos límite que se nos puedan ocurrir.

Para ello tendremos que tener en cuenta algunas consideraciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    class TestableOriginalClass extends OriginalClass {

        public TestableOriginalClass(Session session) {
            super(session);
        }

        @Override
        protected int executeQuery(String query, Object... parameters) {
            // ...
    	    // Our database connection and query execution logic
    	    // ...
        }
    }

3. Continuar el desarrollo

Una vez identificado y testeado el comportamiento de las queries, puede que sea deseable pasar al refactor de las mismas, hacer un poco de scouting, ya que estamos por ahí para aclararnos. Evidentemente lo ideal sería extraer la lógica de las consultas a código, pero no siempre se puede por motivos de tiempo, complejidad o restricciones de hardware.

El refactor más sencillo (y que puede ser de gran ayuda incluso desde el primer paso) es mejorar la legibilidad del string donde se formatea la query.

En este tipo de situaciones, es común encontrarse con una identación confusa o bloques de comentarios de hace años que no aportan nada. Darle un formato limpio que sea fácil de leer, ayuda a identificar mejor el comportamiento de un vistazo y a trabajar de forma más cómoda con la query.

Puede ser tentador pensar en extraer variables de partes del string, como cálculos de negocio, para dar contexto con un buen naming, pero en la práctica puede no ser una buena opción. Por ejemplo, suponiendo el siguiente fragmento de query original:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
...
.append("  CASE ")
.append("    WHEN (field_name = 'some random value') ")
.append("      THEN ")
.append("        CASE ")
.append("          WHEN (field_A <= field_B) ")
.append("            THEN 0 ")
.append("          WHEN (field_A > field_B) ")
.append("            THEN (cast((field_A + 2 * (field_C- field_B)) / 1000 as decimal (19,10)) ")
.append("                  / datediff(dd,date_from,date_to)) )")
.append("        END ")
.append("    ELSE ")
...

Podrían extraerse variables para dar contexto de la siguiente forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
...
String isThisACaseX = " WHEN (field_name = 'some random value') ";
String resultOfSomeBusinessLogic = "cast((field_A + 2 * (field_C- field_B)) / 1000 as decimal (19,10))";
String elapsedDays = "datediff(dd,date_from,date_to)) )";
...
// Dozens of query lines
...
.append("  CASE ")
.append(isThisACaseX)
.append("      THEN ")
.append("        CASE ")
.append("          WHEN (field_A <= field_B) ")
.append("            THEN 0 ")
.append("          WHEN (field_A > field_B) ")
.append("            THEN  " + resultOfSomeBusinessLogic + "/" + elapsedDays)
.append("        END ")
.append("    ELSE ")
...

Y efectivamente, podemos comprobar que la consulta se vuelve más amigable al desarrollador que la está leyendo. El problema es que cuando se necesita saber qué se está haciendo en esos fragmentos de forma concreta, no queda más remedio que dar saltos navegando en el código o dividir la pantalla, impidiendo también seleccionar y copiar toda la query de una sola vez en caso de que necesitemos ejecutarla manualmente.

Puede acabar siendo más práctico añadir pequeños comentarios para dar contexto en puntos concretos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...
.append("  CASE ")
//           Is this business variable on X case? 
.append("    WHEN (field_name = 'some random value') ")
.append("      THEN ")
//               Apply according logic
.append("        CASE ")
.append("          WHEN (field_A <= field_B) ")
.append("            THEN 0 ")
.append("          WHEN (field_A > field_B) ")
.append("            THEN (cast((field_A + 2 * (field_C- field_B)) / 1000 as decimal (19,10)) ")
.append("                  / datediff(dd,date_from,date_to)) )")
.append("        END ")
.append("    ELSE ")
...

Además del formato, también podemos intentar ir granulando los métodos de las clases implicadas para cumplir un poco de SOLID, o refactorizar condiciones o campos que se calculan y luego no se usan, como veremos en el apartado extra.

Tras el refactor, procedemos a añadir la nueva funcionalidad que necesitemos a la query, como siempre añadiendo tests que cubran los nuevos escenarios, y comprobando que no rompemos los tests anteriores (a no ser que cubran casuísticas que ahora dejan de aplicarse).

Extra: Tratar problemas de rendimiento

Al trabajar en un sistema basado en legacy queries que da servicio a un número de usuarias/os concurrente en aumento, con un volumen de datos cada vez mayor, es bastante probable que se acaben dando problemas de rendimiento en la ejecución de flujos de negocio en productivo.

Aunque puede que no haya más que hacer y se trate de que se está llegando a un límite en relación al volumen de datos y tráfico y la capacidad del hardware, puede ser útil tener en cuenta un procedimiento básico a seguir para mejorar el rendimiento:

Publicado el 27/09/2023 por
Borja image

Borja García

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers