Planificando de forma Ágil (2)

November 7, 2006

Después de un breve descanso bloggero (manda narices, después de escribir solo un post :D) prosigo con la segunda parte del post sobre planificación ágil. Si no leíste el primer post sobre planificación ágil, te recomiendo encarecidamente que lo leas. Lo puedes consultar aquí.

Este post esta totalmente basado en uno de Joel Spolsky, concretamente éste; y mi experiencia de haberlo puesto en práctica. Te recomiendo leer no solo ese post, sino cada uno de los posts que tiene en su blog, ¡Son buenísimos!

La solución

¿Planificación Ágil? Sí, es lo que tiene el término “ágil”, ahora todo lo que tenga que ver con el desarrollo de software tiene que ser ágil: metodologías ágiles, testing ágil, agilidad ágil… Así que, ¡Qué demonios! Planificación ágil, o como no morir en el intento de planificar un proyecto.

¿Cómo planificamos? Pues de la forma más fácil…¡Con Excel! No necesitamos tener ninguna herramienta hipercompleja (leáse Ms Project), no necesitamos pagar miles de euros. Lo único que necesitamos es una mínima organización, y esto lo conseguiremos con Excel y una cierta disciplina.

Eres un fanático del Open Source, un vago y maleante que solo utiliza ese sistema operativo de hackers llamado Linux. ¿ Solo se puede planificar ágilmente con un producto de MS ? ¡No! Todo lo que cuento se puede realizar perfectamente con Open Office, de hecho tengo dos versiones, una para OO y otra para Excel. La única limitación es que, aunque OO permite tablas pivotadas, no permite gráficas pivotadas. Pero bueno, esto es un poco pijada (o no…), además supongo que en versiones posteriores de OO incluirán gráficas pivotadas. ¡Así que ya no hay excusa! ¡Arranca tu LFS y planifica tus proyectos de forma ágil con OO!

La forma más sencilla de organizar las tareas de un proyecto sería realizar una lista de TO-DOs (Cosas por hacer), esto lo podemos hacer con un simple fichero de texto, ¡Incluso con papel y boli! (No olvidéis nombrar tan avanzada herramienta la próxima vez que os pregunten en una entrevista que utilizáis para planificar). Aunque una lista de TO-DOs es válido para proyecto pequeños, enseguida vemos que necesitamos “catalogar” dichas tareas, repartirlas entre los miembros del equipo, estimar horas, etc. Por tanto, en vez de utilizar un fichero texto conviene utilizar una hoja de cálculo, lo cual nos va a permitir extender nuestro “modelo de datos” del planificador.

Partiendo de la idea inicial de usar una lista de tareas podemos extender “el modelo” con los siguientes campos:

  • Funcionalidad: Una funcionalidad es algo visible para el cliente, una historia de usuario si nos ponemos XP-ianos. Una funcionalidad va a estar compuesta de tareas.
  • Tarea: Nuestro elemento más básico, un elemento de nuestra primitiva lista de TO-DOs.
  • Número secuencial: Útil para referenciar la tarea de forma unívoca. Imagina que tienes una tarea “Documentación módulo RSP” y otra “Documentación módulo SRP”, ¿Verdad que no querrás que haya una confusión porque alguien creyó leer otra cosa?
  • Estado: ¿La tarea ha comenzado? ¿Se terminó? Es importante guardar el estado de cada tarea. El estado debe ser catalogado (PENDING, STARTED, DONE) para permitir su filtrado. Si además incluimos un formato condicional (Si “PENDING” entonces color rojo) miel sobre hojuelas.
  • Criticidad: ¿Tiene el mismo riesgo poner una imagen en un jsp que la conexión con el backend de nóminas? Seguro que el cliente piensa que cambiar el jsp es más importante, pero esta tarea no tiene ninguna dificultad y la podemos afrontar en cualquier momento. Por tanto asignaremos una criticidad CATALOGADA (LOW, MID, HIGH) a cada tarea. Con esto podremos hacer preguntas tan útiles como: ¿Cuántas tareas críticas están pendientes o sin terminar? Vaya, parece que esto del Excel empieza a dar su juego ehh.
  • Persona: Sería fantástico hacer como Juan Palomo: yo me lo guiso, yo me lo como. Pero normalmente tenemos un equipo de proyecto, y por tanto las tareas las realizarán diferentes personas. Por tanto, añadamos una columna que diga quién realizará la tarea x. Este campo también debe ser CATALOGADO. ¿Por qué? ¿Cuántas formas hay de escribir mi nombre? M. Alcón, Miguel Alcón, Mikel Alcón, Mikel Halcon ( Sí, estoy harto de responder: “Alcón, sin H“). En definitiva, si tenemos un catálogo de desarrolladores, únicamente nos permitirá elegir entre los desarrolladores de la lista. Ahora ya podemos hacer unas cuantas preguntas más a nuestro planificador: ¿Cuántas tareas tiene pendientes el desarrollador X? ¿Quién tiene las tareas críticas?, etc.
  • Estimación original: ¿Cuánto tiempo estimamos que durará la tarea? Planifica siempre en horas y no asignes más de 30 o 40 horas a una tarea, cuantas menos mejor ¿Por qué? Para obligarte a definir el plan con un nivel alto de detalle. Recuerda que huíamos de las planificaciones del estilo “Diseño 2 semanas”. Es más fácil acertar si sabemos exactamente qué vamos a realizar, en esto consiste realmente la planificación ¿Verdad?
  • Estimación real: El que tiene boca se esquivoca equivoca, y planificando te equivocarás, y al principio mucho más. Así que actualiza la estimación real a medida que avanza la tarea. Si pensabas que la tarea x iba a durar 10 horas, pero ahora, con el framework Hiberspring AOP Edition 3.0.1.5, lo puedes realizar en 5 horas; o al revés, te has encontrado con un imprevisto, ¡Actualiza la estimación real! Nos permitirá tener una visión real del estado de la tarea.
  • Tiempo consumido: ¿Cuántas horas llevas consumidas en la tarea? Este campo nos permitirá preguntas tan interesantes como: ¿Cuanto le queda a esta tarea? (Estimación real – Tiempo Consumido) ¿Cuánto le queda al proyecto en general? (SUM (Estimación Real) – SUM (Tiempo Consumido)) ¿Cuántas horas le quedan en tareas asignadas al desarrollador X?
  • Restante: Hemos visto que el tiempo que le queda a una tarea es Est. Real – Tiempo Consumido. Por tanto añadamos una columna con ese valor calculado automáticamente.
  • Desvío: Entre la estimación original y la real hay una diferencia. ¿Cuántas horas más ha costado esta tarea? Precalculemos el valor Estimación real – Estimación original.
  • Desvio %: ¿Es lo mismo retrasarse 2 horas en una tarea de 1 hora que en una tarea de 20? No. Pues entonces precalculemos este valor: Estimación real / Estimación original y lo pondremos en formato porcentual:
    • 100 % : Estimación Real = Estimación Original. Perfecto
    • <100 %: ¡Hemos tardado menos! Cliente contento y nosotros planificaremos de forma más precisa la próxima vez.
    • >100% : Empiezan los problemas, ¿La tarea es crítica? ¿Quién la tiene asignada? Utilizaremos el formato condicional para asignarles colores de fondo dependiendo del desvío (Ej.: Amarillo 101% a 105%, Naranja 105% a 115%, Rojo >115%)

Ahora ya podemos saber cuáles son las tareas que más retrasos están produciendo, tanto en niveles absolutos como porcentuales, sabemos cuáles son los desarrolladores que más se están retrasando, cuánto tiempo le queda al proyecto, etc. En definitiva, tenemos controlado el proyecto.

La cosa podría quedar más o menos así:

planificacion

Como habrás podido comprobar la mayoría de los campos son catalogados. Esto nos permitirá realizar agrupaciones y filtrados de los datos.

¿Se pueden hacer cosas más complejas ? Sí, poniendo fechas de inicio y de fin, tareas bloqueantes, etc. Lo único que tienes que tener en cuenta es que deberás utilizar macros de Excel, no pongas este tipo de campos para ser modificados manualmente, porque conseguirás que te dé pereza actualizar la planificación. Lo campos a rellenar deben ser los mínimos y que no requieran pensar más que en la planificación. Nada de “en qué fecha me pongo si la tarea dura 30 días y hay un fin de semana de por medio”. O utilizas una macro o no utilices estas fechas. Además para la mayoría de proyectos este tipo de fechas no te van a servir para nada. Tu objeto de medida es horas que quedan de desarrollo.

Tampoco hay que emocionarse y empezar a añadir campos a diestro y siniestro. Añade según necesites. De la misma forma, solo añade métricas calculadas que realmente te sean útiles, no añadas algo del estilo (Estimación Original – Estimación Real)*Número de letras del nombre del desarrollador/ PI. Recuerda, puedes ir evolucionando el “modelo de datos” según lo necesites.

Una vez que tenemos la parte técnica resuelta queda la parte metodológica… ¿Cómo coño usamos esto!? Las directrices a seguir serían éstas:

  • Que los desarrolladores que van a realizar las tareas sean los que, generalmente, hagan la estimación original. ¿Por qué? Por dos motivos, el primero que nadie sabe mejor cuanto va a costar hacer un trabajo que la propia persona que va a hacerlo. El segundo motivo es más psicológico: para que la persona asignada se responsabilice de su tarea. Nunca te podrá decir algo como ¡Me diste poco tiempo!.
  • Cada mañana actualiza la planificación. Tu protocolo a seguir cada mañana debería ser el siguiente:
    • Café (Imprescindible, sustituible por té, colacaos, infusiones y bebidas calientes varias. Podrá ser acompañado de galletas, donuts o cualquier artículo de bollería industrial)
    • Reunión con los miembros del equipo, de la siguiente forma:
      • ¡DE PIE! ¿Por qué ? Las reuniones de pie tienden a ir a lo concreto, nadie aguanta mucho sin asentar las posaderas. Si no me crees, compruébalo quedándote de pie 2 horas seguidas, es una experiencia única.
      • Con pizarra. Ayuda a exponer lo que tenemos en mente sin perder el tiempo ni hacérselo perder a los demás. Todo queda más gráfico con un dibujo y minimiza el esfuerzo del resto del equipo para comprendernos.
      • Cada miembro expone el estado de sus tareas y las modificaciones a la planificación (Estimación real y consumido). El jefe de proyecto actualiza la planificación.
      • Se asignan nuevas tareas
      • ¿Dudas? ¿Bloqueos entre tareas? Se comentan y se anotan en la hoja Excel.
    • El jefe de proyecto actualiza la hoja Excel y recalcula las métricas.
    • Consultar las métricas para ver el estado del proyecto, si no es periódicamente por lo menos hacerlo regularmente.
  • Repito, planifica a nivel de horas, no caigas en la tentación de hacer planificaciones a nivel de días, semanas o incluso peor, meses. Perderás el control del proyecto y te estarás autoengañando.
  • Evita los bloqueos entre tareas. No querrás tener desarrolladores parados porque la tarea x no ha terminado, ¿Verdad?
  • Evita que los recursos compartidos se conviertan en elementos bloqueantes:
    • ¿La BD, que usa todo el equipo, se ha caído? ¿Alguien ha cambiado el modelo y los demás no tienen dichos cambios? Seguro que te suena este tipo de problemas. Pon una BD para cada desarrollador, si no puedes permitírtelo crea protocolos que eviten o minimicen estos problemas.
    • Cada desarrollador debe poder ejecutar el código en su propia máquina (Sea un exe, un aplicación web con Apache o con un servidor de aplicaciones por cada desarrollador). No hay excusas, el coste es ridículo.
    • ¡Usa control de versiones! Lo mismo, no hay excusas. Y que nadie, absolutamente nadie suba algo al repositorio sin haber probado tanto sus cambios como la integración con el resto.
    • Relacionado con lo anterior. Genera pruebas unitarias, de regresión, validación, integración etc. Que puedan ejecutarse de manera automática. Imprescindible. Si lo unimos con el punto anterior tenemos que: Nadie, absolutamente nadie, suba algo en el repositorio sin que las pruebas pasen correctamente. He visto equipos de más de cinco personas paradas porque la versión en el SCM tenía un fallo bloqueante.
  • Evita los bloqueos dentro de tareas: Este es la principal fuente de retrasos en los proyectos (Además de los proyectos mal planificados, of course). Programadores que se quedan atascados durante horas con un problema. Haz ver a los miembros del equipo que no están solos en su cruzada, si alguien se bloquea durante más de 15 minutos con un problema ¡Que solicite ayuda a alguien del equipo! Tampoco es cuestión de que el equipo entero se ponga a buscar durante dos horas el carácter “<” que falta en un html, pero generalmente alguien conoce la solución porque ya se enfrentó con ella antes. Eso sí, con un límite, no es cuestión de que la misma persona esté ayudando todo el rato a los demás, dejando su trabajo de lado, o de que se pase dos días intentando solucionar el problema de otro.
  • Añade una tarea “debug”, que sirva de colchón ante imprevistos. Al final del proyecto haz la resta Estimación Original Debug – Desviación Horas Total Proyecto, te ayudará a dar un tamaño correcto a la tarea colchón en siguientes proyectos. ¿Qué tamaño debe tener? Debería tener, al menos inicialmente, un tamaño no menor al 50% de horas del proyecto.
  • Utiliza las tablas pivotadas para calcular totales y subtotales (Total de horas por desarrollador, total de horas por tarea,etc.). Usa las gráficas pivotadas: de un vistazo podrás ver el estado del proyecto.

Espero que estos consejos os permitan planificar mejor (o simplemente planificar). Lee el artículo de Joel, es mucho más completo. Además, yo intento escribir con gracia, pero solo él lo consigue :).

Queda pendiente de subir un fichero Excel de ejemplo como adjunto. Dada mi nulidad blogeril, tendré que investigar cómo hacerlo.


Planificando de forma Ágil (1)

October 11, 2006

El problema

En una de las entrevistas de trabajo que hice hace tiempo me preguntaron que herramienta usaba para diseñar. Yo, ni corto ni perezoso, le respondí: Papel y boli. El entrevistador pareció sorprendido, quizás esperaba una respuesta del tipo “Uso Rational Rose junto con Together más {ponga aquí su herramienta favorita}”. Seguro que estas herramientas son muy útiles, de hecho yo las he utilizado más de una vez, pero creo que en este mundillo hay una sobrevaloración de las herramientas frente a las personas. Parece que es más importante saber utilizar el Rational Rose que saber hacer buenos diseños.

¿Y por qué cuento todo esto? Porque en el terreno de las planificaciones de software hay una herramienta, de esas “imprescindibles”, llamada MS Project que todo Jefe de Proyecto “debe” de conocer. Se usa para todo: Las ofertas llevan un project asociado, una vez iniciado el proyecto el jefe del proyecto se hace otro project, a su vez este pide a sus desarrolladores un project. ¡Y ya tenemos el belén montado! Nadie actualizará esas planificaciones en la vida, su único objetivo era el de aparecer en un papel para justificar que la entrega es sí o sí para el día X.

Las planificaciones con Project nacen viciadas desde su origen. Primero nos ponen una fecha y nosotros hacemos el paripé para calzar todo lo que tenemos que hacer en esa planificación.

Me encantan esas planificaciones del tipo:

  1. Análisis (2 meses) (Consultor Pepito)
  2. Diseño (4 meses) (Analista de 2ª Juanito)
  3. Desarrollo (4 meses)
    1. Web (2 meses)
      1. Gestión de usuarios (2 semanas) (Programador Menganito)
      2. Portal de acceso al empleado (2 semanas) (Becario Fulanito)
      3. Procesos de backend (1 mes) (¿Adivinan? ¡Fulanito y Menganito!)
      4. etc.
  4. etc.

¡Que bien!, nuestra planificación está lista y milagrosamente cumple los plazos acordados con el cliente (O aquella moto que vendió el comercial de turno tomándose unas cañas plácidamente con el cliente).

Pero analicemos detalladamente esta planificación:

  • Nace viciada de origen. Nuestro único objetivo fue cumplir los plazos, por eso utilizamos una herramienta basada principalmente en fechas (Gantt).
  • No está basada en criterios racionales: ¿En que nos basamos para planificar los procesos de Backend en un mes? ¿Hemos tenido una revelación divina? ¿Rappel está en nuestro equipo de proyecto? Este tipo de planificaciones me recuerda a un tipo de mi barrio, un poco raro, que decía que iba a hacer un software de diseño de circuitos que iba a ocupar…¡ 14 diskettes! ¡Y no sabía programar! La diferencia entre esta persona y nosotros es que, nosotros no vemos una media de 5 ovnis al día cuando tomamos el café en la terraza, ni creemos en puertas interestelares (¿No dije que era un tipo raro!!??). Y por tanto debemos ser más serios con lo que hacemos.
  • No es detallada: Por la causa anterior. Una planificación no se debe hacer nunca a semanas, ¡ Y menos a meses! Hacer una planificación no detallada es como decirle al cliente “Mira, no tengo ni idea de cuanto vamos a tardar, pero te voy a mentir para que te quedes más tranquilo. El proyecto ya se retrasará luego, como todos” (¡Lo peor es que con muchos clientes funciona!).
  • Es una referencia/restricción, no una herramienta: Una vez hecho, rara vez se actualizará (siendo muy optimistas). Solo servirá como herramienta de presión para los integrantes del equipo (esto daría para otro post). ¿Cual es el estado del proyecto? ¿Cuanto falta para acabar? ¿Que desarrollador tiene más carga de trabajo? Nuestro project no nos responderá a ninguna de estas preguntas. No está actualizado y esta lleno de suposiciones (benditos eufemismos). Vaya paradoja, ¡La herramienta de planificación no nos sirve para planificar!
  • No tenemos soporte para feedback: Al planificar nos vamos a confundir. Seguro. No existen las planificaciones que encajan como anillo al dedo. Solo podemos minimizar el desvío. ¿Crees que puedes hacer una planificación para el año que viene el mismo día a la misma hora? Cuando planificamos a grandes escalas de tiempo es inevitable confundirse. Influyen muchos motivos: No conocemos bien los requisitos, la tecnología es nueva, etc. Aunque en los siguientes proyectos no podremos presagiar muchos de los problemas que encontraremos, si que podemos prever ciertos desvíos. Por ello es importante almacenar las horas que realmente se imputaron a cada tarea. Debemos aprender de nuestros errores.

Entonces, ¿No vale para nada el MS Project? ¿Hay que huir de ellos como el demonio? Yo no llegaría a tanto, simplemente hay que tener en cuenta sus limitaciones y las implicaciones que tiene. En ciertos proyectos grandes, con muchas dependencias, puede estar justificado su uso. Pero para proyectos pequeños y medianos existen mejores herramientas. ¡Pero esto lo dejo para el siguiente post!


¡Bienvenidos!

October 10, 2006

Mi buen amigo Mikel se ha animado a colaborar compartiendo en este blog sus conocimientos y experiencias en este mundo del desarrollo de software. ¡Me siento muy afortunado de tenerte por aquí! Además, y después de estar detrás de él durante una temporada para que abra un blog, otro amigo mío, Colin, se ha decidido finalmente y ya he podido disfrutar de su primer post. Excelente. Por cierto, el diseño del site muy chulo. ¡Calidad desde Nueva Zelanda!

Para acabar, como reza el título de este post … ¡Bienvenidos!


Ahí queda eso #2

September 21, 2006

5 Dirty Old Town (The Pogues)

“Existen dos formas de diseñar software: una es hacerlo tan simple que obviamente no existen deficiencias, y la otra es hacerlo tan complicado que no existen deficiencias obvias”

C.A.R. Hoare


Registros y Strategy Pattern

September 17, 2006

5 The kids aren´t alright (Offspring)

Introducción

No sé si será un problema común al que un programador se enfrente a lo largo de su vida, pero durante mi carrera ya me he topado con él un par de veces y, a raíz de un artículo refiriéndose al tema en cuestión, he decidido escribir un algo acerca de ello. Hablo del parseo de registros (record parsing). En concreto, del parseo de registros en archivos de textos, los cuales pueden contener varios tipos de de estos registros sin un orden prefijado y uno por línea.

Registros, registros

Por ejemplo, imaginemos que necesitamos recopilar información acerca de personas físicas. Para ello tenemos dos tipos de registros, uno que contiene el nombre y apellido de la persona, y otro que contiene el nombre, apellido, dirección y ciudad de residencia. Una de las formas más comunes de abordar el problema de cómo diferenciar los distintos tipos de registros es utilizar una cabecera en cada uno, indicando de qué tipo es. Veamos un ejemplo de posible entrada:

01234567890123456789012345678901234567890123456789...
--------------------------------------------------...
ONEMichael Smith
TWOTeddy Richardson New York Fifth Avenue

Como se puede apreciar, se utilizan los 3 primeros caracteres de la línea para definir el tipo de registro. En este caso tenemos el tipo ONE, y el tipo TWO. A parte de eso, los diferentes tipos de registros no tienen más campos en común. El objetivo final es obtener el conjunto de registros que recibimos en el fichero, de forma que luego podamos hacer con ellos lo que necesitemos. Muchas veces, proyectos de integración con sistemas antiguos -mainframes especialmente- se abordan compartiendo la información entre éstos y los sistemas nuevos mediante este tipo de ficheros basados en un registro por línea.

Una vez que ya tenemos algo parecido a una especificación informal de la definición del problema y qué necesitamos hacer para resolverlo, vayamos con el cómo. La primera forma que se te ocurre de resolver esto es de sentido común: un bucle que recorre el archivo de texto línea por línea, comprueba qué tipo de cabecera es (un if por cada tipo nos servirá), y obtiene los campos en base a ese tipo. Visto gráficamente:

Parsing flow

La siguiente será la representación de nuestro modelo, del cual queremos crear objetos a partir de los valores presentes en los registros del archivo de texto. Consta de una clase abstracta y dos concretas que extienden de la primera. Se han llamado RecordXXXXX, pero podrían haberse llamado Person o PersonAddress. Hay que recordar que de líneas de texto lo que obtenemos finalmente son objetos reales cuyas propiedades corresponden a los valores parseados en forma de registros. Un aspecto interesante aquí es que las clases no contienen un miembro type que defina qué tipo de registro es. Esto entra en el dominio del parseo y es por tanto responsabilidad de la clase parseadora. Simplemente, ¡no estaría bien aquí!

Basic Model

Y el código asociado:

public abstract class Record
{
}  

public class RecordTypeONE extends Record
{
  public String firstName;
  public String lastName;
}

/* I am not a RecordTypeONE, hence do not attempt to make me inherit from it */
public class RecordTypeTWO extends Record {
  public String firstName;
  public String lastName;
  public String city;
  public String address;
}

Nota: para el que se haya alarmado, los campos de las clases los he mantenido public, sin encapsular, con fines de no añadir más complejidad al ejemplo (echa un vistazo al método setValue() de la clase FieldExtractor más abajo, y sabrás por qué).

Y, finalmente, éste es el método que sirve para parsear el conjunto de posibles registros que recibimos en el fichero de texto.

// dentro de una clase...
public List process()
{
  List records = new ArrayList<Record>();
  while (!EOF())
  {
    String line = getNextLine();
    Record record = null;
    if (line.substring(0, 3).equals("ONE")
    {
      record = new RecordTypeONE();
      record.firstName = line.substring(3, 11);
      record.lastName = line.substring(11, 29);
    }
    if (line.substring(0, 3).equals("TWO")
    {
      record = new RecordTypeTWO();
      record.firstName = line.substring(3, 9);
      record.lastName = line.substring(9, 30);
      record.city = line.substring(30, 39);
      record.address = line.substring(39, 60);
    }
    else
    {
      throw new InvalidRecordException("Record type not recognized");
      // or just remove this else block in order to ignore unknown record types
    }
    records.add(record);
  }
  return records;
}

Como vemos, esta solución es bastante sencilla. En principio, es suficiente, y la mayoría de las veces nos empecinamos -yo el primero- en intentar hacer las cosas más complicadas por nuestra tendencia natural a creer que las soluciones sencillas no son las mejores. Sin embargo, es todo lo contrario, las soluciones sencillas son las más efectivas y las más difíciles de dar con ellas. Esto es así porque la simplicidad da respuesta al mayor problema al que se enfrentan la computación en la actualidad: la gestión de la complejidad. Pero esa es otra historia.

Con lo que hoy nos toca, tengo que decir que, aunque sí que es cierto que esta solución al problema es correcta y hasta deseable, puede y debe mejorarse. Esto es así porque por naturaleza, el parseo de registros es cambiante. Estad seguros de que la probabilidad de que se produzca uno de los dos cambios siguientes en los requisitos es muy elevada:

  1. Gestión de los tipos de registros (nuevos, eliminaciones)
  2. Cambio en la composición de un registro (nuevos campos, eliminaciones, cambios de lugar)

Este es uno de esos casos en los que defenderse ante estos cambios es más económico que gestionarlos dentro del propio método, bien añadiendo o quitando bloques condicionales, modificando el contenido de éstos para hacer frente al punto 2 expuesto anteriormente, etc. Una de las herramientas más potentes con las que cuenta un programador utilizando un lenguaje orientado a objetos son los Patrones de Diseño. En pocas palabras,son soluciones documentadas a problemas de diseño conocidos que han funcionado en el pasado y siguen haciéndolo en la actualidad. Veamos un ejemplo de uno de los más sencillos y útiles, aplicándolo a nuestro problema de los cambios potenciales en el parseo de registros.

Introducción al Strategy Pattern

Este patrón es de los primeros que se explican en la mayoría de cursos. Básicamente se utiliza para aislar el comportamiento de un objeto del propio objeto. Se ve bastante claro en la siguiente figura:

Strategy Hierarchy

El comportamiento del que hablábamos, esto es, la funcionalidad que provee el método perform(), se ha externalizado a una nueva jerarquía simple de clases, de forma que Context simplemente llama a este método, sin saber cómo se ejecuta. Y aquí está la clave, como Context no sabe qué implementación se ejecuta (de hecho, le da igual), se puede cambiar la implementación en cualquier momento, incluso en tiempo de ejecución, entre las diferentes estrategias (Strategy1, etc).

Ventajas de hacerlo así, son varias:

  1. Como hemos dicho, se puede cambiar la implementación en cualquier momento
  2. Está más estructurado el conjunto de implementaciones, ya que cada una está en una clase. Esto
    falicita el mantenimiento. Se pueden añadir nuevas implementaciones y quitar o modificar las existentes de una forma muy limpia y eficiente
  3. Es útil para reducir el número de clases en el sistema. Si varias clases son prácticamente la misma
    pero difieren en el comportamiento, a lo que ayuda este patrón es a tener una única clase que utiliza
    las diferentes estrategias (comportamientos)
  4. Por supuesto, como veremos más adelante, nos ahorramos ese conjunto de bloques condicionales que
    siempre ensombrecen la elegancia de nuestro código

El Strategy Pattern en acción

Veamos ahora como encaja este patrón en nuestro ejemplo. La clase FieldExtractor enlaza una ristra de caracteres (con índices superior e inferior) con la propiedad correspondiente en un objeto Record. Un método interesante de esta clase es setValue(), ya que utiliza la Reflection API para establecer el valor de la propiedad del objeto Record, que como se verá después, el objeto FieldExtractor sólo conoce en tiempo de ejecución.

public class FieldExtractor
{
  protected int start;
  protected int end;
  protected String propertyName;
  public FieldExtractor(int start, int end, String propertyName)
  {
    this.start = start;
    this.end = end;
    this.propertyName = propertyName;
  }
  public void extractField(String line, Record record)
  {
    String value = line.substring(start, end + 1);
    setValue(record, value);
  }
  // Reflection API to the rescue!
  protected void setValue(Record record, String value)
  {
    Field field = record.getClass().getField(propertyName);
    field.set(record, value);
  }
}

La intefaz RecordReader define los métodos destinados al parseo de registros a partir de líneas de texto, usando objetos FieldExtractor para dicha función.

public interface RecordReader
{
  String getType();
  void addFieldExtractor(FieldExtractor fieldExtractor);
  Record processLine(String line);
}

La siguiente implementación básica da una idea más específica del funcionamiento del parseo de un registro, dada una línea de texto. Como se puede apreciar, mantiene referencias a un conjunto de objetos FieldExtractor, y cuando el método processLine() es invocado, los recorre todos, de forma que al final se obtiene un objeto Record con todos sus campos rellenos a partir de la línea de texto. También contiene referencias a la cadena de texto que define el tipo de registro que se encarga de parsear, así como su clase.

public class RecordReaderImpl implements RecordReader
{
  protected String type;
  protected Class recordClass;
  protected List fieldExtractors = new ArrayList();
  public RecordReaderImpl(String type, Class recordClass)
  {
    this.type = type;
    this.recordClass = recordClass;
  }
  public String getType()
  {
    return type;
  }
  public void addFieldExtractor(FieldExtractor fieldExtractor)
  {
    fieldExtractors.add(fieldExtractor);
  }
  public Record processLine(String line)
  {
    Record record = recordClass.newInstance();
    for (FieldExtractor extractor : fieldExtractors)
    {
      extractor.extractField(line, record);
    }
    return record;
  }
}

La ventaja de todo esto es que, para cada tipo de registro, no vamos a tener que definirnos una clase de forma separada, o cómo hacíamos antes, definir un nuevo bloque condicional asociado a su parseo. En cierto sentido, hemos cambiado creación por configuración. Es decir, en vez de crear un montón de clases parseadoras específicas a cada tipo de registro, simplemente tenemos una única clase, lo bastante flexible como para poder crear a partir de ellas objetos parseadores de forma dinánima. Y la de código -y mantenimiento- que nos hemos ahorrado.

Todo lo anterior no es muy útil sin un cliente que haga uso de ello. Para ello nos definimos una clase RecordProcessor, que será la encargada de recorrer línea por línea el archivo de texto y obtener finalmente el conjunto de objetos Record.

public class RecordProcessor
{
  RecordReaderConfiguration config;
  public RecordProcessor()
  {
    config.setUp();
  }
  public List process()
  {
    List records = new ArrayList();
    while (!EOF())
    {
      String line = getNextLine();
      RecordReader reader = config.getRecordReader(line);
      Record record = reader.processLine(line);
      records.add(record);
    }
    return records;
  }
}

Como vemos, RecordProcessor es bastante sencilla, invocando para cada línea a un objeto RecordReader, visto anteriormente. Lo interesante aquí es cómo se obtienen estos objetos RecordReader en tiempo de ejecución. Para ello, la clase RecordProcessor mantiene una referencia a un objeto RecordReaderConfiguration.

public class RecordReaderConfiguration
{
  protected Map recordReaders;
  public void setUp()
  {
    addRecordType1();
    addRecordType2();
  }
  public RecordReader getRecordReader(String line)
  {
    String type = line.substring(0, 3);
    RecordReader recordReader = recordReaders.get(type);
    if (recordReader == null) throw new IllegalArgumentException("Type does not exist");
    return recordReader;
  }
  protected addRecordType1()
  {
    RecordReader recordReader = new RecordReaderImpl("ONE", RecordTypeONE.class);
    recordReader.addFieldExtractor(new FieldExtractor(3, 11, "firstName"));
    recordReader.addFieldExtractor(new FieldExtractor(12, 28, "lastName"));
    recordReaders.add(recordReader.getType(), recordReader);
  }
  protected addRecordType2()
  {
    RecordReader recordReader = new RecordReaderImpl("TWO", RecordTypeTWO.class);
    recordReader.addFieldExtractor(new FieldExtractor(3, 11, "firstName"));
    recordReader.addFieldExtractor(new FieldExtractor(12, 28, "lastName"));
    recordReader.addFieldExtractor(new FieldExtractor(29, 40, "city"));
    recordReader.addFieldExtractor(new FieldExtractor(41, 59, "address"));
    recordReaders.add(recordReader.getType(), recordReader);
  }
}

Aquí es donde se hace el trabajo duro de parseo. El método setUp() -llamado por RecordProcessor en su constructor- se encarga de añadir los objetos RecordReader que parsearán el conjunto de todos los registros que pueden encontrarse en el fichero de texto. Estos objetos son almacenados en un Map, de forma que las keys del Map las forman los strings representando los distintos tipos de registro, y los values los forman a su vez los objetos RecordReader.

Finalmente, este es el aspecto que tiene el conjunto de clases e interfaces que he definido más arriba:

Final Structure

Gestión de cambios

Veamos cómo afectarían los cambios a esta nueva disposición de clases basadas en el Strategy Pattern.

1. Gestión de los tipos de registros (nuevos, eliminaciones)

Añadir un nuevo tipo, es tan sencillo como registrar un nuevo tipo de RecordReader en la clase RecordReaderConfiguration

2. Cambio en la composición de un registro (nuevos campos, eliminaciones, cambios de lugar)

Basta con modificar el cuerpo de los métodos en los que se crean los RecordReaders de la clase RecordReaderConfigurationaddRecordTypeXX()

Notas finales
Supongo que todos aquellos fans -con razón- de los frameworks IoC (Spring, PicoContainer, MicroContrainer,etc), se habrán percatado de la gran ventaja de tener la configuración de los RecordReaders dentro de una clase separada (RecordReaderConfiguration). Os dejo para vosotros un asunto pendiente tan interesante como externalizar dicha configuración a una fuente externa (XML?) e inyectarla a la clase RecordProcessor.

Bibliografía