miércoles, 29 de abril de 2009

Refactoring y Cómo corregir sin Destruir

Muchas veces me toca, en conjunto con mi equipo, resolver problemas, o las bien llamadas Incidencias, que no son detectados durante la etapa de QA y que impactan a clientes que están en producción. Típicamente, las correcciones de este tipo, conocidas como Parches y/o Fixes, involucran una intervención de código que está en producción, sin mucho tiempo para grandes especificaciones y, por último, con un cliente ansioso por una solución.

En estos escenarios, es necesario ejecutar un proceso como el siguiente:

  • Identificar el Error
  • Replicar el Error
  • Analizar e Identificar la Causa
  • Corregir el Error
  • Probar y Certificar la Solución
  • Publicar

Dependiendo de la complejidad del problema, algunas etapas se pueden omitir, sin embargo, típicamente, la etapa de corrección siempre está presente (a menos que el problema sea el usuario y/o esté entre el teclado y la silla).

Haciendo una analogía, esto es casi como lo que realiza un cirujano, es decir, abrir un cuerpo vivo (el software), tomar una decisión (analizar e identificar la causa), extraer, implantar y/o limpiar (modificar el código fuente), cerrar el cuerpo (compilar y probar) y dar de alta (publicar). Por suerte en el caso del software, casi nunca, está la vida de alguien de por medio.

El problema es que hacer una intervención para corregir un código sin mucho tiempo para probar, ni hacer pruebas de regresión, etc., no es una tarea fácil. La intervención debe asegurar que no hay efectos colaterales y, aunque no es infalible, hay algunas guías para realizar esto. Esto es lo que se conoce como Refactoring que, según la definición, es una intervención en un software para mejorar su legibilidad y/o su estructura sin modificar el resultado. Veamos algunos ejemplos.

Ejemplo 1. Funciones y Redefiniciones

Supongamos una función como la siguiente para validar si un string es un número entero:

int validaEntero( String sText ) {
 try
 {
  return Integer.parseInt( sText );
 }
 catch ( Exception ex )
 {
  return 0;
 }
}

En la declaración anterior se asume que el 0 es un valor de error y, supongamos, que se detecta un escenario en donde el valor 0 es válido. Claramente la función no permite detectar directamente si viene un 0 y/o un valor erróneo en la entrada. El primer impulso para corregir esto es, o debiera ser, agregar la posibilidad de incorporar un valor por default, por ejemplo:

int validaEntero( String sText, int iDefault ) {
 try
 {
  return Integer.parseInt( sText );
 }
 catch ( Exception ex )
 {
  return iDefault;
 }
}

Un programador inexperto, probablemente modificaría la misma función ya existente produciendo un desastre con otras partes del código que utilicen la función sin el valor por default. Un programador con algo más de experiencia, probablemente agregaría la función anterior generando dos funciones prácticamente idénticas en el código pero con firmas distintas. Ahora bien, un programador consciente de un proceso de refactoring reutilizaría la nueva función en la primera para mantener de alguna manera la funcionalidad concentrada como sigue:

int validaEntero( String sText ) {
 return validaEntero( sText, 0 ); // 0 Valor por default
}

int validaEntero( String sText, int iDefault ) {
 try
 {
  return Integer.parseInt( sText );
 }
 catch ( Exception ex )
 {
  return iDefault;
 }
}

Ejemplo 2. Páginas Web y Parámetros

Supongamos una página web que recibe un parámetro, como resultado de un POST HTTP, para eliminar una actividad como sigue (el parámetro como GET sólo para facilitar la descrpción):

http://X.X.X.X/eliminaActividad.jsp?id=9820

Esta página es utilizada por varias páginas de un sitio web para realizar esta acción (incluso, con AJAX, por ejemplo). En un contexto determinado, un programador decide que va a reutilizar la página para permitir eliminar una lista de actividades y, apresuradamente, decide modificar la página asumiendo que la entrada ahora va a ser como sigue:

http://X.X.X.X/eliminaActividad.jsp?id=9820,7645,7282

Si esta corrección se realiza mal (ver Inducción), lo más probable es que la página ya no funcione correctamente cuando se envíe un único Id de actividad. El proceso de intervención correcto tiene dos posibles soluciones:

  • Agregar un parámetro nuevo para proveer la lista de actividades, por ejemplo, idl (id List) y, según el parámetro provisto, realizar la acción correspondiente.
  • Procesar el parámetro id y determinar si corresponde o no a una lista de id´s de actividad.

Ejemplo 3. WebServices y Publicación de Métodos

Supongamos un WebService determinado que recibe dos parámetros a y b y que retorna la suma de a y b (nada muy complejo obviamente). Al igual que el ejemplo 1, se determina que se debe agregar un tercer parámetro para indicar si los parámetros a y b son números y/o representaciones hexadecimales de los mismos. En este caso, la inspección de código no permite dimensionar las consecuencias de esto porque no necesariamente se tiene control sobre los sistemas que estén integrados con este servicio "fundamental".

La secuencia de solución es similar a la indicada en el Ejemplo 1 en donde, básicamente, se debe ampliar la funcionalidad existente de manera de agregar un comportamiento nuevo.

El gran desafío entonces de este tipo de correcciones es lograr resolver el problema sin provocar efectos secundarios y esto, desde mi punto de vista, es un hábito que se debe aprender y practicar.

Hay un excelente artículo de con varias guías y cómo mejorar un código existente en Java en la revista JavaWorld

lunes, 27 de abril de 2009

Teorema de Inducción Matemática y Software

Para los que han tenido formación matemática, el Teorema de Inducción Matemática se utiliza para demostrar que una fórmula determinada es válida para un determinado universo de valores. En general, se enseña en el contexto de la reducción de series de números a fórmulas discretas, por ejemplo, con inducción se puede demostrar fácilmente que:

  • 1 + 3 + 5 + 7 + ... + (2n -1) = n^2 para todo n >= 1
  • 2 + 6 + 10 + ... + (4n - 2) = 2*n^2 para todo n >= 1

Este teorema es interesante porque permite de una manera simple demostrar que algo se cumple siempre, siguiendo la secuencia siguiente:

  • Demostrar para n = 0 ó 1 (según corresponda)
  • Asumir que es válido para n = n
  • Demostrar que se cumple para n = n + 1

En el mundo del software aplicado (no teórico) por suerte no hay nada que demostrar, sólo hay que programar, sin embargo, el concepto de hacer el análisis para valores conocidos, extrapolar y luego volver a hacer el análisis para valores extremos se puede utilizar fácilmente para mejorar las estrategias de programación.

Ejemplo 1 - Parámetros de un JSP
Supongamos una página JSP que recibe un parámetro que corresponde al id de una base de datos. Por ejemplo:

http://X.X.X.X:8080/procesaItem.jsp?itemID=56353

El código para procesar esta solicitud, obligatoriamente, debe tener una instrucción como la siguiente:

long iItemID = Long.parseLong( request.getParameter("itemID") );

La instrucción anterior está expuesta a muchos errores (itemID == null, itemID = "", itemID = "A,B", etc.), pero, continuando con lo expuesto acá, la primera condición de validación es sobre la existencia o no del parámetro itemID. Por lo tanto, aplicando la secuencia del teorema de inducción descrita antes para el parámetro itemID, tendríamos que:

  • Recuperar el parámetro para n = 0 (es decir, no viene)
  • Suponer para n = n
  • Recuperar el parámetro para n = n + 1 (es decir, viene más de una vez)

El caso n=0 es simple de resolver, pero, es necesario dejar establecido cuál es el comportamiento en esta situación.

El caso n=1 es el caso de éxito en estricto rigor y debiera ser simple de resolver.

El caso n=n+1, es más complejo y obedece a una llamada como la siguiente:

http://X.X.X.X:8080/procesaItem.jsp?itemID=56353&itemID=616516

En este caso, a menos que funcionalmente esté considerado algo así, la página producirá un error. Fácilmente, aplicando el proceso descrito, un programador podría percatarse de esta situación y hacer la página casi indestructible (no voy a describir cuál es la solución acá).

Ejemplo 2 - Extracción del username de un e-mail.
Supongamos que en un software se requiere obtener el username del e-mail ingresado por el usuario. Por ejemplo, de una entrada del tipo perico@prueba.com se requiere obtener "perico".

Una solución inmediata (y muy común) es la siguiente:

String sUserName = sEmail.substring( sEmail.indexOf("@") );

Si bien se resuelve el problema, este código está lleno de vulnerabilidades dado que el E-Mail es ingresado por un usuario/sistema externo y, por lo tanto, no se puede hacer ningún supuesto respecto a él (para los que han programado en Java, saben que las funciones para manipular Strings son poderosas pero a la vez muy caprichosas). Los errores posibles serían básicamente que sEmail == null, sEmail = "", sEmail = "A,B", etc.

Si recordamos el teorema de inducción, podremos fácilmente mejorar el código anterior. Claramente la variable que determina el éxito en la operación es la existencia del @. Por lo tanto, aplicamos rápidamente la secuencia del teorema considerando n como el número de ocurrencias del @ y obtenemos:

  • Recuperar el username para n = 0
  • Suponer para n = n
  • Recuperar el username para n = n + 1

En este caso, n = 0 puede ser y es responsabilidad del programador identificar el caso base. Considerar n = 1 como caso base es un error a menos que el contrato de la función indique que el email siempre traerá un @ (en mi experiencia, siempre es mejor ponerse en el peor caso). Con lo anterior, es necesario definir claramente el resultado esperado para la primera afirmación y para la última dado que son condiciones de borde.

Claramente el teorema de inducción no sirve para demostrar nada en el contexto de la programación aplicada, sin embargo, siguiendo la secuencia descrita aquí y basándose en él, creo que se logra gatillar un proceso de reflexión simple y eficiente para blindar el software ante las miles de condiciones de error que pueden ocurrir durante la ejecución... en particular... cuando el software pasa a manos de los usuarios (qué mágica capacidad que tienen de destruirlo en un segundo, je, je)

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.

martes, 7 de abril de 2009

Proceso de Selección - Parte I

Como parte importante de mi trabajo, habitualmente me toca la difícil y compleja tarea de buscar personas para que se integren a mi equipo. En la estructura actual, una de mis labores es el desarrollo de productos nuevos y la mantención correctiva de productos antiguos. No somos una empresa muy grande y muchos menos una empresa con estructuras muy verticales, por lo que, en general, busco personas que puedan tener una visión más amplia de los problemas, es decir, que puedan moverse libremente desde la Especificación de Requerimientos hasta la Puesta en Producción pasando por Desarrollo y Pruebas.

Es un hecho que no todas las personas pueden (y quieren) cumplir con este desafío, pero, para las que si lo desean hacer, es un requisito básico que tengan una buena base de programación. Sin esto, difícilmente, podrán levantar la cabeza del bit y el byte para entender aspectos más importantes del problema, del negocio y/o del cliente.

Anteriormente, he participado de varios procesos de selección en los cuáles se realizan preguntas sueltas y/o exámenes para verificar, certificar, los conocimientos de programación. En lo personal, siempre he sentido que las empresas que realizan esto, se acercan un poco a un proceso de selección más objetivo, por lo tanto, decidí aplicarlo en mi proceso de selección.
El proceso de entrevista es el normal (revisión del CV, entrevista personal, etc.) más la realización de algunas preguntas de programación. Dado que el lenguaje que utilizamos principalmente es Java, la búsqueda y las preguntas están orientadas a personas con experiencia en este lenguaje.

La primera pregunta (muy simple por lo demás), consiste en determinar cuál es la salida, en tiempo de ejecución, del siguiente programa:

Para mi sorpresa, esta pregunta casi siempre la contestan mal. No tengo claro si es la sorpresa de la situación, la presión, lo inesperado y/o definitivamente que no les enseñan esto en los planes de estudio actuales (claramente, esto no debiera ser un capítulo en ninguna parte, con algunas horas bastaría para poder enseñar esto).

Bueno, pero, en definitiva, ¿cuál es el problema? ¿porqué no es inmediata la respuesta? básicamente por dos razones:

  • Las personas no conocen los operadores ++, --, etc.
  • Las personas no tienen claro la precedencia en los operadores.

Para enteder claramente la pregunta, es necesario conocer y entender los puntos anteriores. Sin entrar mucho en detalles, los operadores ++ y --, son conocidos como operadores de incremento y decremento y reemplazan las instrucciones siguientes:

  • i++ es equivalente a i = i + 1
  • i-- es equivalente i = i - 1

El segundo aspecto importante para entender la pregunta es la precedencia de estos operadores. Para esto:

  • Si los operadores se ubican antes, se realiza el incremento/decremento y se retorna el valor.
  • Si los operadores se ubican después, se retorna el valor y se realiza el incremento/decremento.

Lo más importante en esta sintaxis, es que el valor actual de la variable a la que se le aplica el operador se altera siempre. Por ejemplo:



  • En la primera instrucción, primero se asigna el valor de i a la variable j y luego se incrementa ( i = i + 1).
  • En la segunda línea, primero se incrementa i (i = i + 1) y luego ase asigna el valor de la variable a j.
  • En la tercera línea, se asigna el valor de i a j sin modificar el valor de i.

Desde que comencé mis primeros pasos en programación en la universidad (en lenguaje C), estos operadores y la sintaxis asociada fueron recalcados como importantes y fundamentales. Especial relevancia toman estos operadores en el caso de los ciclos for, por ejemplo, las siguientes declaraciones producen el mismo resultado:

Nuevamente, la compresión del operador (++ en este caso) facilita la lectura del código y, por el contrario, la no compresión de él, podría indicar la utilización de él exclusivamente por memoria y/o copy & paste lo que claramente no es recomendable.

¿Está claro, ahora si, cuál es el resultado en tiempo de ejecución del programa?