JavaTM EE (J2EE) Programming (with Passion!)

He comenzado este curso online de JavaEE, del koreano Sang Shin. Le vi en un par de presentaciones en la Java One en Madrid este año. La verdad que el tipo es impresionante dando presentaciones. Genial.

Con respecto al curso, no puede recomendarlo, porque ha comenzado esta semana, así que si aún no te has apuntado, estás a tiempo. Sólo te exige 2-4 horas como mucho a la semana. Nada como volver con aquello que crees saber para descubrir nuevas cosas y afianzar conocimientos.

Exceptions y JDK 1.3

Yo soy el primero al que le encantaría el poder utilizar las características de la versión 1.5 de Java, en concreto encuentro muy útiles los static imports y los generics, y por supuesto las annotations. Sin embargo, durante mi carrera, y por diversas razones, me he encontrado en situaciones de no poder utilizar siquiera la versión 1.4 del JDK. Es triste, pero cierto, y me temo que es muy habitual encontrarse con la restricción tan temida de: “Tenemos que programar contra el JDK 1.3“. Es algo así como una especie de maldición, cuando estás habituado a utilizar los métodos de la clase String añadidos a partir de la versión 1.4 -replaceAll(), split(), etc-, o la facilidad de la exception chaining. Por fortuna, el proyecto Commons de la fundación Apache puede ayudarnos a solventar algunos de esos obstáculos. Vayamos con la exception chaining. Esta es la interfaz de la clase java.lang.RuntimeException* en la versión 1.4:

public RuntimeException()
public RuntimeException(String message)
public RuntimeException(String message, Throwable cause)
public RuntimeException(Throwable cause)

Como se puede observar, dos de los constructores de esta clase admiten un argumento java.lang.Throwable, por tanto cuando usemos esta clase en la versión 1.4 no perderemos la traza de llamadas de las excepciones. Ahora veamos las signaturas de los constructores de esta misma clase en la versión 1.3:

public RuntimeException()
public RuntimeException(String message)

Ahora ya sabemos que la exception chaining fue un añadido a la versión 1.4 del JDK a la clase RuntimeException. En el subproyecto Commons Lang existe una jerarquía de clases que nos ayudan a establecer una pila de llamadas previa en el constructor de una excepción. En concreto, son las clases NestableRuntimeException y NestableException del paquete org.apache.commons.lang.exception:

Jakarta Commons Exception Hierarchy

Estos son los constructores de la clase NestableRuntimeException:

public NestableRuntimeException()
public NestableRuntimeException(String message)
public NestableRuntimeException(String message, Throwable cause)
public NestableRuntimeException(Throwable cause)

Equivalentes a los de la clase RuntimeException, de forma que funciona como tal y además mantiene una referencia a un objeto Throwable, con lo que simulamos la funcionalidad añadida a la versión 1.4 pero en un entorno compilado para la 1.3. Ahora simplemente podremos definir nuestras propias subclases de NestableRuntimeException o NestableException, sin miedo a que obtengamos errores de compilación debido al overriding de los constructores no existentes en la 1.3, pero sí en la 1.4. Ahora que lo digo, yo he sufrido dichos errores debido a que en mi IDE (Eclipse normalmente) no configuro el proyecto para compilar las fuentes contra la 1.3 (cuando ha de desplegarse en un entorno de JDK 1.3, claro). Aunque ya voy aprendiendo. Considero siempre una buena práctica configurar el entorno de desarrollo a semejanza del entorno destino, en la medida de lo posible.

* También se aplica a la clase java.lang.Exception

Planificando de forma Ágil (1)

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!

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!

Rompiendo Dependencias

5 Freestyler (Boomfunk MC)

Introducción

En un mundo ideal, todos comenzaríamos los proyectos desde cero, podríamos elegir las tecnologías que consideráramos más apropiadas para cada caso, los jefes nos entenderían, nosotros entenderíamos a los jefes, etc. En fin, un Mundo Feliz como el de Aldous Huxley. Sin embargo, para todo el que lleva algo de tiempo en el mundo del desarrollo de software (más o menos con su primer día de trabajo después acabar los estudios ya es suficiente) sabe lo que le toca: mantener código. Según lo veo yo, a esta actividad normalmente se la considera de segundo orden, y no lo es. La confusión es que muchos creen que mantener código pasa casi exclusivamente por cambiar cosas ya hechas o arreglar errores en el código, ya sea tuyo o de otros. Vamos, un sufrimiento. Pero mantener código es mucho más que eso, ya que la propia expresión cambiar cosas ya hechas es demasiado general. Puede abarcar desde cambios en la interfaz de usuario, añadir funcionalidad nueva a la ya existente, eliminar funcionalidad no requerida, mejora del rendimiento, separación y creación de componentes a partir del código actual para ser reutilizado, cambios en el sistema de construcción o despliegue de la aplicación, etcétera etcétera
Como se puede observar, las habilidades requeridas pueden ser muy heterogéneas y abarcar varios campos, e incluso podría decir que es hasta fascinante. Porque al fin y al cabo, la gran mayoría de las aplicaciones evolucionan una vez implantadas hasta que ya no son necesarias, y alguien tiene que hacerse cargo de esa evolución.

Mantenimiento en la Práctica

Muchas veces nos encontramos con código más o menos antiguo, al cual tenemos que añadir funcionalidad nueva o modificar la existente. El problema es que este código es totalmente nuevo para nosotros y no queremos romper nada. Normalmente esto sucede debido a un fuerte acoplamiento entre los componentes del sistema, en un sentido más o menos general: clases, métodos, etc. Un cambio simple en la implementación de un método de una clase puede afectar, transitivamente, a una clase de la que ni siquiera tenemos constancia de su existencia. Puede que el comportamiento erróneo introducido lo descubramos cuando la aplicación está en ejecución, en entornos de QA o Producción. Dependiendo de la tolerancia del proyecto al ciclo de feedback, podemos encontrarnos con una situación inaceptable. Y he aquí la cuestión principal a tener en cuenta: ¿cómo podemos saber si hemos introducido efectos colaterales no deseados en otras partes del sistema? Evidentemente, un sistema software actual puede llegar a ser muy complejo, pero existen formas de mitigar los efectos no deseados de nuestras modificaciones al código. En concreto, estoy hablando de los tests, tanto unitarios como de integración en este caso. Si tenemos un conjunto de tests que prueban la corrección del código existente, podremos sentirnos más seguros a la hora de añadir o modificar funcionalidad. Es por eso que la recomendación en este caso es la de crear ANTES tests para el código sobre el que debemos trabajar. Nos lo agradeceremos nosotros mismos, y nos lo agradecerá el resto del equipo.

Dependencias

Después de haber intentado dejar claro que un buen juego de tests son una especie de seguro de vida, vayamos a su vez con uno de los problemas que nos encontramos cuando queremos crear un test unitario para una clase de este sistema: las dependencias. Algo tan simple como este código puede ser un obstáculo tremendo para acometer lo que nos proponemos:

class A
{
  private B b = new B();

  public void doWhatever()
  {
    b.doBTask();
  }
  ...
}

La clase A tiene una relación de dependencia con la clase B, lo que significa que en el método doWhatever() de A se utiliza la funcionalidad de B. Si la dependencia fuera de uso estaríamos en un caso parecido. Ahora, si queremos crear un test unitario para dicho método de A, estaremos indirectamente invocando al método doBTask() de B. En dos palabras, ya no sería un test unitario. Por tanto, centremos nuestro objetivo en el título de este post: romper dependencias. Aunque primero presentemos un ejemplo un poco más real, que a veces eso de tener clases A y B, más que ayudar, complica la explicación.

De camiones, ruedas y motores

Imaginemos un sistema de información de una empresa de transportes. Cada cierto período de tiempo, esta empresa realiza exhaustivos controles mecánicos a su flota de camiones. Esta podría ser la parte del sistema que nos interesa (obviamente muy simplista):

Trucks, Wheels ...

La clase Truck podría tomar la siguiente forma en el código:

class Truck
{
  private Wheel[] wheels;

  private Engine engine;  public Truck()
  {
    wheels = new Wheel[4]{new Wheel(), new Wheel(), new Wheel(), new Wheel()};
    engine = new Engine();
  }

  public boolean review()
  {
     boolean firstWheelOK = wheels[0].check();
     boolean secondWheelOK = wheels[1].check();
     boolean thirdWheelOK = wheels[2].check();
     boolean fourthWheelOK = wheels[3].check();
     boolean engineOK = engine.check();

     return (engineOK && firstWheelOK && secondWheelOK && thirdEngineOK && fourthEngineOK);
  }
}

La clase anterior, evidentemente, es mejorable en muchos aspectos: ¿4 ruedas fijas?, ¿clase inmutable?, etc. Pero a efectos de ilustrar la rotura parcial de dependencias, nos sirve. Vayamos con el test. Esta técnica es independiente del framework de testing que utilicemos (JUnit en este ejemplo).

class TruckTest extends Test
{
  private Truck truck;

  public void setUp() throws Exception
  {
    truck = new Truck();
  }

  public void testCheck() throws Exception
  {
    assertTrue(truck.review());
  }
}

La llamada truck.review(), implica que se llaman los métodos check() de las ruedas y del motor del camión. Esto sería correcto para un test de integración, pero no para uno unitario. ¿Qué podemos hacer? Pues bien, para todos aquellos afortunados que en su empresa utilizan un framework de IoC, podéis dar por finalizada la lectura de esto aquí. Delegar en Spring por ejemplo la creación y asignación de los objetos miembros de la clase Truck (wheels y engine) es ciertamente algo deseable. Sin embargo, si una aplicación no hace uso de la inversión del control, ya sea por antigüedad o por la razón que sea, mi predicción es que en muchos casos migrar el código para delegar el ciclo de vida de las dependencias a un framework externo como Spring puede ser complicado y producir más de un dolor de cabeza. Así que valgámonos de una propiedad de la OO como es el polimorfismo para crear un test unitario para la clase Truck.

El primer paso va a ser refactorizar la clase para que obtenga las referencias de sus miembros a través de un método accesor (getWheels() y getEngine()):

class Truck
{
  private Wheel[] wheels;

  private Engine engine;  public Truck()
  {
    wheels = getWheels();
    engine = getEngine();
  }

  public boolean review()
  {
    boolean firstWheelOK = wheels[0].check();
    boolean secondWheelOK = wheels[1].check();
    boolean thirdWheelOK = wheels[2].check();
    boolean fourthWheelOK = wheels[3].check();
    boolean engineOK = engine.check();

    return (engineOK && firstWheelOK && secondWheelOK && thirdEngineOK && fourthEngineOK);
  }

  protected Wheel[] getWheels()
  {
    return new Wheel[4]{new Wheel(), new Wheel(), new Wheel(), new Wheel()};
  }

  protected Engine getEngine()
  {
    return new Engine();
  }
}

El segundo paso va a ser crear clases mock de dichas dependencias, de forma que estáticamente podemos definir el comportamiento de éstas y poder así probar la clase Truck en base a este comportamiento definido:

class TruckTest extends Test
{
  private Truck truck;  public void setUp() throws Exception
  {
    truck = new MockTruck();
  }

  public void testCheck() throws Exception
  {
    assertTrue(truck.review());
  }

  protected class MockTruck extends Truck
  {
    protected Wheel[] getWheels()
    {
      /* 1 */
      create and return my own wheels!!!
    }

    protected Engine getEngine()
    {
      /* 2 */
      create and return my own engine!!!
    }
  }
}

Como se puede ver, la clase interna MockTruck es idéntica en comportamiento y estructura a la clase Truck, excepto que sobreescribe los métodos accesores de las dependencias creando y devolviendo las ruedas y el motor cuyos estados definimos nosotros (/* 1 */ y /* 2 */). Con esto hemos conseguido aislar parcialmente la creación dichas dependencias, por lo tanto los tests sobre la clase Truck que definamos suponen un estado y comportamiendo fijo de dichas dependencias. Hemos aislado parcialmente a Truck y ahora sí que TruckTest puede considerarse un test unitario.

Conclusiones

Un problema de uilizar esta técnica de cara al futuro puede ser que queramos crear tests de Truck para diferentes estados de sus dependencias. Tendríamos que crear una clase MockTruck por cada juego de comportamientos, o externalizar la creación de dichas dependencias en los accesores de la clase interna MockTruck, o podríamos … En fin, varias posibilidades. Por supuesto, existen frameworks muy útiles para crear mocks dinámicamente, como jMock o rMock, pero puede ser también complicada su introducción en un sistema no concebido inicialmente para su testing unitario. Por ejemplo, si no se ha seguido la máxima programar contra interfaces, esto es, que no existan o que se programe a través de ellas. Pero vamos, que se decida lo que se decida son respecto al código legado, es preferible tener al menos algo como lo de arriba, a no tener anda.

Algo como Mantenimiento Dirigido por Test, puestos a aportar algo a todo ese océano de siglas con que nos abruman cada día a los programadores.

Ahí queda eso #2

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

Subversion y Regreso al Pasado

5 A forest (The Cure)

Introducción

A veces, más de las que quisiéramos, hacemos un commit en nuestro scm que resulta, después de haberse hecho efectivo, un commit no deseado. Y todas esas veces uno se pregunta: ¿cómo puedo deshacer los cambios que he subido al repositorio? Veamos cómo se hace con la herramienta scm que utilizo en mi día a día: subversion. Nota: se presuponen conocimientos previos del uso y funcionamiento de esta herramienta.

Ejemplo

Para intentar ilustrar esto de la mejor forma posible, disponemos de un árbol de directorios y ficheros representando los contenidos tanto del repositorio (shared copy) como de la copia local (working copy). La revisión actual asociada a dicho árbol es la número 30, y tanto la copia local como el repositorio se encuentran sincronizados, por tanto, ambos en la revisión 30.

myproject tree

En la figura se puede ver que las fuentes del proyecto son muy sencillas, una clase java bajo src/ y un archivo de configuración de Ant, ambos bajo la rama principal del repositorio (trunk). También, siguiendo las convenciones de subversion, se dispone de un directorio para las tags del proyecto (tags/) y otro para las ramas (branches/).

Merging en subversion

Realmente el término merge no tiene en subversion su significado clásico: aplicar los cambios de una rama en otra. En subversion funciona de una forma un poco distinta, y básicamente lo que hace es comparar dos ramas y aplicar las diferencias a una copia local destino. Más formalmente, en una operación merge entran en juego 3 participantes:

  • El árbol del repositorio inicial (left side)
  • El árbol del repositorio final (right side)
  • Una copia local (target) sobre la que se aplican los cambios obtenidos de comparar leftside y rightside

Un aspecto que tiene que quedar claro, es que después del merge, tendremos en la copia local la diferencia entre ambos árboles en la forma de CAMBIOS SALIENTES. Posteriormente, deberemos hacer un commit de dichos cambios, o si no estamos de acuerdo, realizar una operación revert en la copia local.

El siguiente comando obtendría la diferencia entre la revisión número 15 de la rama experimental1 en el repositorio y la revisión número 30 -la última- de la rama principal, y posteriormente aplicaría dicha diferencia a la copia local myproject/:

$ svn merge http://localhost/svn/myproject/branches/experimental1@15
            http://localhost/svn/myproject/trunk@30 myproject/

Lo más importante aquí es comprender que podemos comparar cualesquiera versiones de cualesquiera dos árboles en el repositorio y aplicar la diferencia resultante a una copia local. Imaginemos que queremos comparar dos revisiones de un mismo árbol y aplicar el resultado de la comparación a nuestra copia local. Podríamos hacer:

$ svn merge -r29:30 http://localhost/svn/myproject/trunk myproject/

Mi principal objetivo no es dar una explicación exhaustiva del comando merge, y si tienes más interés en esto, recomiendo una visita a la sección de enlaces más abajo.

Haciendo cambios

Durante tu trabajo del hoy has tenido que cambiar la clase MyApp.java. El cambio consiste simplemente en que has puesto explícitamente un directorio asociado exclusivamente a tu máquina. La clase compila y pasa los tests manuales (sic). Y haces un commit de la clase.

$ svn commit -m "Changed directory path"
Sending        src/MyApp.java
Transmitting file data ...
Committed revision 31.

Ahora es demasiado tarde para no hacer saltar las alarmas, y a los cinco minutos ya tienes a un compañero en tu puesto quejándose de tu “imprudencia”.

Ouch! Deshaciendo cambios

¿Y qué puedo hacer? Lo primero que se te ocurre es obtener una copia de la revisión anterior (30) del fichero MyApp.java, sustituirlo en tu copia local y realizar un nuevo commit. Esto funciona para un fichero, para dos, para tres. Cuando nos encontramos con que el commit consiste en decenas de cambios, con ficheros añadidos, eliminaciones, reemplazos, modificaciones, etc el parche manual no parece tan atrayente, ¿verdad? Debe de existir una forma más sencilla de hacerlo. Lo que se explicó en las secciones anteriores nos servirá ahora para comprender cómo se deshace un commit no deseado. Para ello nos servimos de la capacidad del comando merge para comparar dos revisiones cualesquiera de dos árboles del directorio cualesquiera.

$ svn merge -r31:30 http://svn.example.com/repos/calc/trunk
U  src/MyApp.java

Por si se ha escapado: -r31:30 ¡Estamos obteniendo el resultado de comparar la revisión 31 con la 30! Es decir, la revisión origen es más reciente en el tiempo que la revisión destino. A esto se le llama una comparación backwards. Ahora tienes a MyApp.java marcado como modificado en tu copia local, de forma que puedes hacer un commit, y obtener el mismo árbol asociado a la antigua revisión número 30.

$ svn commit -m "Undoing commit"
Sending        src/MyApp.java
Transmitting file data ..
Committed revision 32.

Evidentemente, dado que subversion lleva un control de todo el historial de cambios, si alguien hace un checkout de la revisión 31 de la rama principal, obtendrá el fichero MyApp.java con la ruta del fichero escrita explícitamente. Pero ahora en el HEAD de la rama principal tenemos lo que realmente queremos, y dado que en la mayoría de los desarrollos lo que interesa es la estabilidad de la rama principal, es un efecto secundario sin gran repercusión.

Enlaces

Registros y Strategy Pattern

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 RecordReaderConfiguration -addRecordTypeXX()-

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

Ahí queda eso #1

5 Points of Authority (Linkin Park)

“El problema de esta empresa es que hay gente que tiene muy buenas ideas, y cuando después de dos años ven que no se ponen en práctica, se van a otro sitio”

Development Manager Anónimo