viernes, 17 de abril de 2009

Si no tienes éxito...

Habitualmente me toca ayudar a mi equipo en las labores de desarrollo que realizan. Esto implica, muchas veces, revisar código con ellos para ver dónde puede estar el error no encontrado y/o revisar la lógica implementada, en casos muy específicos, para certificar que esté correcta.

Como parte de este recorrido infinito, siempre encuentro aspectos que creo se podrían mejorar como parte de las técnicas de programación. Imposible es para mí en este momento pensar en documentarlas de alguna manera mejor (un libro por ejemplo), razón por la que comenzaré a registrar mis descubrimientos y reflexiones aquí.

El otro día revisando un código, me encontré algo como lo que sigue:

if( COND1 ) {
  if( COND2 ) {
    if( COND3 ) {
      :
      calculaValores(...);
      :
    }
  }
}

El gran problema del código anterior es la mantenibilidad. Básicamente porque la inclusión de nuevas condiciones, obliga al programador a tener visibilidad del bloque completo (gran problema cuando estos if anidados miden más de 40 líneas). Por ejemplo, supongamos que es necesario agregar un nuevo camino a la segunda condición de la siguiente manera:

if( COND1 ) {
  if( COND2 ) {
    if( COND3 ) {
      :
      calculaValores(...);
      :
    }
  }
  else {
    // Condición Nueva
    calculaValores2(...);
  }
}

Automáticamente se puede apreciar que el código deja de ser mantenible porque seguir la secuencia se hace más complejo (vuelvo a insistir, especialmente cuando estos if anidados tienen muchas líneas e instrucciones más complejas). Además, la condición nueva, que se supone debe ejecutarse si y sólo si no se va a ejecutar la 3, queda sintácticamente después, lo que visualmente induce a error. Para evitar esto hay dos alternativas simples de implementar como describo a continuación.

Solución 1 - Evaluar la condición de error primero
Esta solución contempla la evaluación de aquellas condiciones que inhiben el procesamiento de las siguientes primero. Según esto, el código anterior podría re-escribirse como sigue:

if( COND1 == false ) {            // CASO A
  return;
} if( COND2 == false ) {
  return;
} if( COND3 ) {
  :
  calculaValores(...);
  :
}

if( COND1 == false ) {            // CASO B
  return;
} else if( COND2 == false ) {
  return;
} else if( COND3 ) {
  :
  calculaValores(...);
  :
}

El caso a) aplica rápidamente en aquellas condiciones en que la evaluación está en el contexto de una función y/o procedimiento que permita el uso de la instrucción "return" sin afectar el resto del código (por ejemplo, no aplicaría para un servlet). Si no se puede hacer un "return", entonces, el caso b) funcionaría mejor. Volviendo al ejemplo anterior, si fuera necesario modificar la lógica para incorporara un nuevo camino en la segunda condición, el código se puede modificar fácilmente como sigue:

if( COND1 == false ) {             // CASO A
  return;
}

if( COND2 == false ) {
  calculaValores2(...);
  return;
}

if( COND3 ) {
  :
  calculaValores(...);
  :
}

if( COND1 == false ) {              // CASO B
  :
} else if( COND2 == false ) {
  calculaValores2(...);
} else if( COND3 ) {
  :
  calculaValores(...);
  :
}

El caso a) sólo aplica si es posible utilizar el return, el caso b) si no.

Solución 2 - Flags de Ejecución
La segunda alternativa de solución es mediante lo que yo denomino "flags de ejecución" y que corresponde a variables booleanas que permiten controlar el flujo. Volviendo al ejemplo anterior, esto se lograría de la siguiente manera:

boolean bCond1 = false;
boolean bCond2 = false;
boolean bCond3 = false;

bCond1 = COND1;
if( bCond1 == false ) {
   return;
}

bCond2 = COND2;
if( bCond2 == false ) {
   return;
}

bCond3 = COND3;
if( bCond1 && bCond2 && bCond3 ) {
   :
   calculaValores(...);
   :
}

A primera vista, el código se ve más complejo, sin embargo, lo relevante acá es la posibilidad de saber en todo momento cuáles de las condiciones aplican para tomar decisiones (especialmente relevante cuando el código es muy largo), entonces, la incorporación del nuevo camino sería como sigue:

boolean bCond1 = false;
boolean bCond2 = false;
boolean bCond3 = false;

bCond1 = COND1;
bCond2 = COND2;

// Condicion de fracaso 2
if( bCond1 && ( bCond2 == false ) ) {
   calculaValores2(...);
}

bCond3 = COND3;
// Condición de éxito 1, 2 y 3
if( bCond1 && bCond2 && bCond3 ) {
   :
   calculaValores(...);
   :
}

Caso Especial - Condiciones Relacionadas
Hay una situación especial que a veces ocurre y es lo que llamo Condiciones Relacionadas. Esto básicamente tiene que ver con que a veces una condición sólo puede determinarse si es que la anterior tuvo éxito. Por ejemplo, la condición 2 sólo puede calcularse si la condición 1 es exitosa. Esta condición sólo aplica para el caso de los flags de ejecución, ya que en el caso de la Solución 1, la sintaxis del if se hace cargo de esto. En una situación como ésta, el código anterior se vería como sigue (considerando esto para la condición 2 y 3):

boolean bCond1 = false;
boolean bCond2 = false;
boolean bCond3 = false;

bCond1 = COND1;
// Evaluación de la Condición 2
if( bCond1 ) {
   bCond2 = COND2;
   if ( bCond2 == false ) ) {
     calculaValores2(...);
   } else {
   // Solo se puede calcular la Condición 3 si es que la 1 fue exitosa
     bCond3 = COND3;
   }
}

// Condición de éxito 1, 2 y 3
if( bCond1 && bCond2 && bCond3 ) {
   :
   calculaValores(...);
   :
}

Probablemente, los expertos en Ingeniería de Software estén pensando que todos estos problemas se podrían haber evitado con un buen diseño y, bueno, yo también lo creo. Sin embargo, lo que describo acá se basa en aquellos escenarios de "cirugía", en los cuáles el diseño ya se hizo, se requiere hacer una corrección y no hay muchas alternativas para rediseñar y/o reconstruir todo. Esto es lo que se conoce como refactoring (hay un libro muy bueno que describe esto) en el mundo del software.

No hay comentarios.: