martes, 26 de mayo de 2009

La Misión

El otro día en una compra-venta de autos encontré un pedazo de madera, sobre el escritorio del vendedor, que tenía el siguiente poema de Gabriela Mistral:
Donde haya un árbol que plantar, plántalo tú
Donde haya un error que enmendar, enmiéndalo tú
Donde haya un esfuerzo que todos esquivan, hazlo tú
Sé tú el que aparta la piedra del camino
Esto se escapa habitualmente a mis publicaciones, sin embargo, me sentí plenamente identificado por él, así que decidí compartirlo por acá. Lamentablemente, aún no logro encontrar ninguna referencia al material de Gabriela Mistral para poder leerlo completamente.

jueves, 21 de mayo de 2009

Proceso de Selección - Parte II

Siguiendo con el proceso de selección, la segunda pregunta que he hecho a los candidatos como parte del proceso de entrevista es la siguiente.

Considerando la siguiente jerarquía de excepciones qué excepciones son válidas para completar la declaración del método foo() en la clase B (donde está la X).


  class A {
  :
  public foo(...) throws D {
    :
  }
  :
}

class B extends A {
  :
  public foo(...) throws X {
    :
  }
  :
}

En términos generales, esta pregunta intenta evaluar los conceptos básicos de herencia en la Programación Orientada a Objetos. Muchos de los candidatos se sienten intimidados e intentan responder (pocas veces exitósamente) aunque no tengan claro el concepto.

Para poder responder la pregunta, lo primero que hay que tener claro es la herencia. Mirando la declaración de excepciones se puede concluir que:

  • D es una excepción de tipo C
  • E es una excepción de tipo D y de tipo C

El concepto importante acá (en relación a la herencia) es que no necesariamente esto se pude intepretar al revés, es decir, que C es una excepción de tipo E. Una manera simple de ver esto es, por ejemplo, en la siguiente herencia:

Lo que se puede concluir inmediatamente es que:

  • Un perro es un animal
  • Un labrador es un perro

Sin embargo, no necesariamente aplica en el sentido inverso, por ejemplo, un perro no necesariamente es un labrador.

Tomando en cuenta todos estos elementos, las excepciones válidas para ser declaradas en X son D y E. Básicamente, D porque es equivalente en términos de la firma al método de la clase base y E porque es una excepción de tipo D también. Esto es lo que se conoce como Especialización.

martes, 5 de mayo de 2009

Te declaro por un rato...

Hace tiempo me tocó subirme al carro de la programación en Java y, en especial, programación J2EE con toda la parafernalia asociada (mdb´s, ejb´s, etc.). Yo tengo una base en programación de software sobre lenguajes estructurados (C, Pascal) y Orientados a Objetos (C++) principalmente. Adicionalmente, por inquietud profesional, es decir, por los pitutos que he realizado, tengo experiencia en lenguajes del mundo Microsoft previos a .net (Visual Basic 6.0, ASP, etc.) y, ahora último, C#. Estos últimos conocidos como "El Lado Oscuro" por mis pares amantes de las infraestructuras del tipo LAMP. En este camino de descubrimiento de las ventajas y bondades de la programación en Java, uno de los aspectos que más me molestó es la insoportable sintaxis y/o estándar de facto relacionado a la programación J2EE para el uso de recursos y/o objetos determinados.

Un claro ejemplo de esto es el uso de mecanismos de Log en estos ambientes. Una sintaxis típica de una instrucción de Log en este contexto se ve como sigue:

  :
FactoryManager.getLogFactory().getLogger().Log("Ha ocurrido un error");
  :

A simple vista, no tiene mucha complejidad este código, sin embargo, si vemos un código que haga uso extensivo de esta sintaxis se vería así:

FactoryManager.getLogFactory().getLogger().Log("Comenzado proceso de Analisis");
FactoryManager.getLogFactory().getLogger().Log("Analizando Bloque 1");
  :
FactoryManager.getLogFactory().getLogger().Log("Analizando Bloque 2");
  :
FactoryManager.getLogFactory().getLogger().Log("Proceso de Analisis Terminado");
FactoryManager.getLogFactory().getLogger().Log("Bloques Correctos " + ....);
FactoryManager.getLogFactory().getLogger().Log("Bloques con Error " + ....);

Desde mi punto de vista, este código tiene los siguientes problemas:

  • Genera más código del necesario (en lo visual y en bytes).
  • Es innecesariamente complejo dado que no facilita procesos de mantención asociados, en este caso, a la clase Log, LogFactory y FactoryManager. Básicamente, porque un cambio menor en cualquiera de ellas, podría rápidamente impactar las 7 líneas anteriores. Peor aún cuando el código sobre el que se está operando tiene miles de líneas.
  • No hace ni fomenta un correcto control de errores para la obtención del LogFactory ni para la recuperación del objeto Logger (aunque debieran ser implementaciones con un Singleton y/o variables static, cualquiera de estas dos podría retornar un error y la sintaxis no lo refleja ni aborda).

Para evitar estos problemas, mi recomendación es el uso de variables de referencia, es decir, variables temporales destinadas única y exclusivamente a resolver los problemas anteriores. Estas variables no tienen ningún impacto en la lógica implementada.

El código anterior podría reescribirse como sigue:

LogFactory oLF = FactoryManager.getLogFactory();
Logger oLog = oLF.getLogger();

oLog.Log("Comenzado proceso de Analisis");
oLog.Log("Analizando Bloque 1");
  :
oLog.Log("Analizando Bloque 2");
  :
oLog.Log("Proceso de Analisis Terminado");
oLog.Log("Bloques Correctos " + ....);
oLog.Log("Bloques con Error " + ....);

Si incorporamos la validación de condiciones de error para los objetos indicados antes, el código quedaría como sigue:

LogFactory oLF = FactoryManager.getLogFactory();

if( oLF == null ) {
  // Determinar qué hacer en esta condicion de error
}

Logger oLog = oLF.getLogger();
if( oLog == null ) {
  // Determinar qué hacer en esta condicion de error
}

oLog.Log("Comenzado proceso de Analisis");
oLog.Log("Analizando Bloque 1");
  :
oLog.Log("Analizando Bloque 2");
  :
oLog.Log("Proceso de Analisis Terminado");
oLog.Log("Bloques Correctos " + ....);
oLog.Log("Bloques con Error " + ....);

Desde mi perspectiva este código es más claro, fácil de mantener y visualmente simple. En el caso de clases que se utilicen reiteradamente y que el uso del log sea una obligación, si el acceso al "Logger" no presenta grandes problemas, una clase podría definir internamente una referencia al Log para ser utilizada por todos los métodos de la misma como sigue:

class A {
  LogFactory oLF = null;
  Logger oLog = null;

  public A() { // Constructor
    oLF = FactoryManager.getLogFactory();

    if( oLF == null ) {
      // Determinar qué hacer en esta condicion de error
    }

    oLog = oLF.getLogger();
    if( oLog == null ) {
      // Determinar qué hacer en esta condicion de error
    }
  }

  void metodoUno(...) {
    :
    oLog.Log("Ejecutando Metodo Uno");
    :
  }

  void metodoDos(...) {
    :
    oLog.Log("Ejecutando Metodo Dos");
    :
  }
}

Lo anterior no aplica para clases con métodos estáticos y/o escenarios en donde el destino del "Log" es dinámico (archivos, base de datos, e-mail, etc.).

Otro ejemplo que me ha tocado ver en donde lo anterior aplica y produce mejoras sustanciales es en el contexto de la programación de Servlets. En lo personal, he encontrado pocas justificaciones para hacer uso de esta tecnología (en vez de JSP), sin embargo, reiteradamente aparece sintaxis como la siguiente:

public class MiServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;

  public void init(ServletConfig servletConfig) throws ServletException {
    super.init(servletConfig);
  }

  protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
    httpServletResponse.getWriter().write("<tr ....");
    httpServletResponse.getWriter().write(" <td ....");
    httpServletResponse.getWriter().write(" </td ....");
    httpServletResponse.getWriter().write(" <td ....");
    httpServletResponse.getWriter().write(" </td ....");
    httpServletResponse.getWriter().write("</tr");
  }
}

Y que, aplicando lo descrito anteriormente, se puede dejar como sigue:

protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {

  java.io.PrintWriter oPage = httpServletResponse.getWriter();

  oPage.write("<tr ....");
  oPage.write(" <td ....");
  oPage.write(" </td ....");
  oPage.write(" <td ....");
  oPage.write(" </td ....");
  oPage.write("</tr");
}

Generando un código bastante más simple y manejable desde mi punto de vista.

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?

martes, 31 de marzo de 2009

¿Dónde está el Error?

Actualmente, el lenguaje de programación sobre el que estamos desarrollando nuestros productos es Java. Algunas de las principales ventajas de este lenguaje son el, casi-siempre-válido, write-once-run-everywhere (escribe una vez, ejecuta en cualquier parte), un entorno de que apoya muy bien el proceso de desarrollo (eclipse por ejemplo) y las infinitas bibliotecas (o librerías como prefieren llamarlas algunos por error) disponibles en internet para resolver problemas específicos.

En lo personal, una de las desventajas más importantes que yo siento tiene el lenguaje es el recolector de basura, básicamente porque como estrategia de programación, se trabaja en el escenario de recursos ilimitados, es decir, no hay un uso racional de los recursos del sistema operativo (memoria, disco, filedescriptors, etc.) y, si bien es cierto no es un problema en estricto rigor del lenguaje, si es de la manera en que se enseña a programar en él. Básicamente, porque el uso y administración de los recursos del Sistema Operativo no puede delegarse al recolector. Los que aprendieron a programar en C tendrán claro a qué me refiero con esto.

La otra desventaja que veo es la incorporación de las excepciones. Hasta el momento, no he logrado descubrir un escenario controlable en el que se haga uso de las excepciones y éstas no generen problemas secundarios difíciles de manejar. Los que aprendimos a programar en C, estamos acostumbrados a syntaxis como la siguiente:

char *sFile = ....;
FILE *fp = NULL;
fp = fopen( sFile, "r");

En donde, lo más importante, es que cualquier operación/función tiene un resultado determinado. En este caso la variable fp tiene el resultado de la operación, para este caso particular, NULL (0) en caso de error, <>0 en caso de éxito. En el caso de Java, una instrucción similar, sería la siguiente:

String sFile = ....;
File oFile = new File( sFile );
FileInputStream fis = new FileInputStream( oFile );

Y, la principal diferencia con el código en C , es básicamente que en esas tres líneas se pueden lanzar varias excepciones, produciendo un flujo no-determinado como se esperaba. Particularmente delicado es esto cuando hay recursos del Sistema Operativo en uso, como son los FileDescriptors o Streams de Java que, si no son cerrados adecuadamente, producirán a la larga pérdidas de memoria ya que el recolector de basura no es capaz de determinar correctamente si esos recursos deben o no ser liberados.

Para evitar lo anterior, es necesario "recubrir" el código con la sentencia try{...} catch{...} que resuelve el problema, pero que, a la larga, hace el código más complejo. Algunas de las instrucciones más problemáticas que típicamente causan los mayores problemas son las siguientes:

Ejemplo 1. Integer.parseInt( sValue );
Esta instrucción es bastante ingenua y simple, sin embargo, produce uno de los errores más innecesarios de programación en Java y es que, el código anterior, lanza una excepción si es que el valor sValue no es un valor que se puede interpretar (parsear) como entero en este caso.

Para solucionar esto, hay múltiples respuestas y alternativas, como por ejemplo, validar que el contenido del string sólo tenga números hasta utilizar una expresión regular. Cualquiera de las alternativas anteriores, según yo, complejiza el problema, por lo que la solución, es símplemente construir una función que atrape la excepción y retorne el valor encontrado. Lo importante acá es que efectivamente se use la función en vez de llenar el código de sentencias try{...}catch{...}.

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

Se pueden hacer versiones para tener más precisión (Integer, Long, Double, etc.). Por otro lado, la incorporación del valor por default permite asegurar que programáticamente se pueda definir/determinar la condición de error en la traducción (parsing)

Ejemplo 2. sValue.substring( 0, 10 );
Probablemente, ésta es una de las instrucciones simples y más peligrosas en el manejo de strings. La causa es simple: si el largo del string (sValue en el ejemplo) es menor al largo solicitado (10 en el ejemplo), se lanza una excepción por índice fuera de rango. En términos generales, está claro que las condiciones de borde debieran producir un error pero, el problema, es que en condiciones casi normales, también se produce el error. Además de la función que se podría hacer para controlar el error, lo sorprendente es que, según yo, para el caso más común (string más corto de lo solicitado), no se debería lanzar una excepción y se debiera retornar lo que se pudo.

En términos simples, si se quiere sacar(cortar) los primeros 10 caracteres de un string de 8... ¿Es suficiente con los 8 que hay? ¿Es necesariamente un error, que no haya 10?. La función que permite omitir este error, es la siguiente:

int left( String sText, int iLeft )
{
  if( sText == null || sText.length() == 0 )
    return sText;
  return sText.subString( 0, iLeft );
}

Ejemplo 3. BufferedInputStream bis = new BufferedInputStream( new FileInputStream( sFileName ) );
En este caso, además de los errores indicados anteriormente, hay un error no visible y que tiene que ver con el uso de los recursos del Sistema Operativo. En este caso, la creación del objeto BufferedInputStream a partir del objeto FileInputStream sin la declaración de una variable de por medio, no asegura que el recolector de basura realice el trabajo de liberación de los recursos asociados de manera correcta.

La manera de corregir este error, es declarando todas las variables intermedias. Adicionalmente, hay que asegurarse de declarlas de manera que estén disponibles para la sentencia finally{...} de manera de asegurar el correcto cierre de los recursos.

FileInputStream fin = null;
BufferedInputStream bis = null;

try
{
  fin = new FileInputStream( "c:/test.txt" );
  bis = new BufferedInputStream( fin );
  while( bis.available() > 0 )
  {
    // leer el archivo
  }
}
catch (Exception ex)
{
  // Procesar el error
  System.out.println("ERROR" + ex.getMessage() );
}
finally
{
  // Asegurar liberación de recursos del S.O.
  if( bis != null )
    bis.close();
  if( fin != null )
    fin.close();
}