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 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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: