miércoles, 17 de junio de 2015

Parchear vs Refactorización Continua

Artículo perteneciente a la sección de calidad del código

Hola a todos,

En el mundo de la programación existe un mantra que se repite hasta la saciedad que reza así: "Si funciona, no lo toques que lo estropeas". Esta frase , en general, es errónea y su aplicación indiscriminada implica la duplicación de código, el aumento de complejidad de los proyectos, el incremento de tiempos de testeo y la dificultad creciente para implementar nuevas características a nuestros productos.

Esto no quiere decir que en cualquier momento tengamos que refactorizar nuestro proyecto hasta que todo el código esté como una patena, a veces, los tiempos de entrega son los que son y no disponemos del tiempo suficiente para hacer las cosas como es debido. No obstante, si nos vemos obligamos a parchear lo primero que tendremos que hacer inmediatamente después de la entrega es plantearnos la refactorización del código para eliminar el susodicho parche. Si no, estaremos contrayendo una deuda técnica que con el tiempo se hará insalvable.

Si ya has leído otros artículos míos o de otros blogs sobre calidad de código y mantenibilidad de proyectos de software estarás aburrido de todo lo que he dicho hasta ahora. Es por ello que este artículo se enfoca a explicar esta problemática entre "hacer las cosas deprisa" y "hacer las cosas bien" con un ejemplo muy sencillo.

Imaginemos que trabajamos en una empresa de software que se dedica a hacer software a medida para clientes en javascript (por decir un lenguaje). Un buen día un cliente nos pide que le hagamos una función que devuelva un string de texto con todas las combinaciones que se pueden hacer con 2 ceros binarios y 1 uno binario separada cada combinación por un retorno de carro. Dado que es la primera vez que este cliente nos hace un encargo y que lo que nos pide no es muy complicado lo programaremos sin matarnos demasiado, una cosa sencilla , a facturar y a otra cosa.

La solución más sencilla al problema que nos piden es la siguiente:

function GetBinaryString()
{
   return "001\n010\n100";
}

Un par de dias después nos aparece el cliente y nos dice que ha habido algún tipo de error. Por lo visto el encargado de tomar notas se quedó con la mitad del mensaje. El cliente quería una función que en base a un parámetro que nos pasara nos generase además de la cadena ya pedida , otra cadena que fuera de 2 unos y 1 cero.

No es muy complicado entender que dado el caso que es un cambio relativamente pequeño y el código es minúsculo ( de hecho solo tiene una linea ) se puede hacer un pequeño parche de este estilo:

function GetBinaryString(num_bits_to_1)
{
   if(num_bits_to_1 == 1)
      return "001\n010\n100";
 
   if(num_bits_to_1 == 2)
      return "011\n110\n101";
 
   return "ERROR";
}

La solución tal vez no es muy elegante, pero realmente hacerlo de otra manera, en este caso, sería perder el tiempo.

Otro trabajo bien hecho.

Entonces es cuando empieza a venir la vorágine de cambios diarios. El cliente trabaja para una subcontrata y continuamente le están cambiando las especificaciones así que va pidiendo cada día "pequeños" cambios:

- Que sean 4 bits y no 3 los que están en juego.
- Que los grupos puedan ser 1,2 y 3 unos.
- Que sean 6 bits y no 4.
- Que los grupos puedan ser de hasta 5 unos
-etc etc etc

Delante de estos cambios hay dos opciones.
- Seguir parcheando incluyendo las nuevas especificaciones como buenamente se pueda dejando un código de este estilo
function GetBinaryString(num_bits_to_1, num_total_bits)
{
   switch(num_total_bits)
   {
     case 3: return GetBinaryStringTotal3(num_bits_to_1);
     case 4: return GetBinaryStringTotal4(num_bits_to_1);
     case 5: return GetBinaryStringTotal5(num_bits_to_1);
     case 6: return GetBinaryStringTotal6(num_bits_to_1);
   }
 
   return "ERROR";
}

function GetBinaryStringTotal3(num_bits_to_1)
{
   switch(num_bits_to_1)
   {
     case 1: return "001\n010\n100";
     case 2: return "011\n110\n101";
   }
   return "ERROR";
}

function GetBinaryStringTotal4(num_bits_to_1)
{
   switch(num_bits_to_1)
   {
     case 1: return "0001\n0010\n0100\n1000";
     case 2: return "0011\n0110\n1100\n1001\n0101\n1010";
     case 3: return "1110\n1101\n1011\n0111";
   }
   return "ERROR";
}

function GetBinaryStringTotal5(num_bits_to_1)
{
   switch(num_bits_to_1)
   {
     ..... decenas de combinaciones
   }
   return "ERROR";
}

function GetBinaryStringTotal6(num_bits_to_1)
{
   switch(num_bits_to_1)
   {
     ..... decenas de combinaciones
   }
   return "ERROR";
}

- Refactorizar código y replantearse empezar desde el principio a fin de que el nuevo código cumpla con las nuevas especificaciones con el mínimo de complejidad posible. Esto hace que el código nos quede del siguiente estilo: (no explicaré como funciona, si alguien tiene dudas, que pregunte :) )

function GetBinaryString(num_bits_to_1, num_total_bits)
{
  var pattern = 0;
  var string = "";
 
  pattern = GetPattern(pattern,num_bits_to_1, num_total_bits);
 
  do
  {
     string += DrawPattern(pattern,num_total_bits);
     pattern = GetPattern(pattern,num_bits_to_1, num_total_bits);
  } while ( pattern < (0x01<<num_total_bits) );
 
  return string;
 
}

function Count1(pattern,num_total_bits)
{
  var cont = 0;
 
  for(var i = 0 ; i < num_total_bits ; ++i)
  {
    if(patter & (0x01<<i))
    {
      cont ++;
    }
  }
 
  return cont;
}

function GetPattern(pattern,num_bits_to_1, num_total_bits)
{
     while(true)
     {
       pattern++;
             
       if(Count1(pattern,num_total_bits) == num_bits_to_1)
         return pattern;
     }
}

function DrawPattern(pattern,num_total_bits)
{
  var string = "";
 
  for(var i = 0 ; i < num_total_bits ; ++i)
  {
    if(pattern & (0x01<<i) )
      string += "1";
    else
      string += "0";
  }
  string += "\n";
  return string;
}




Evidentemente el buen camino es el camino de la refactorización dado que nuevos cambios serán absorbidos sin problemas y en algunos casos ya estarán implementados sin querer. En cambio con el parche continuado lo que no esté explicitamente implementado no estará , pero además el coste de mantenimiento será creciente.

Dicho de otra manera, con el tiempo el código parcheado se convertirá en un monstruo de miles de lineas inmantenible (y eso que al principio solo era 1 linea de código), mientras que el código refactorizado ocupará siempre menos de 30 lineas (salvo cambios muy bestias en la especificación).

Como factor adicional está que introducir casos nuevos en el sistema parcheado se tiene que hacer a mano, permitiendo que haya nueva fuentes de error, en cambio en el sistema refactorizado no haría falta ni testing del desarrollo debido a que el código es claro sobre lo que hace y lo que no hace, otorgando un plus de confianza al desarrollo.

Espero que os haya gustado y que lo pongáis en práctica en vuestros proyectos.

Recordad que para cualquier duda aquí estoy.

1 comentario :

Entradas populares