Patrón Iterador

Al igual que el patrón Observador, el patrón iterador tiene dos versiones. Una implementación desde cero o la facilidad de utilizar la interfaz que nos proporciona Java por defecto. Empecemos con la versión creada por nosotros desde cero.

Cuando decimos Iterador, nos estamos refiriendo a recorrer una lista de elementos. En otras palabras poder acceder a cada elemento almacenado en una lista (piensen que una lista en realidad es un contenedor de elementos) sin importar el orden.

Por ejemplo, cuando necesitamos revisar que pagamos todas las facturas del mes o cuando estamos revisando las fotos en un directorio o en un álbum. Para realizar estas acciones, en realidad no nos importa el orden, solo nos importa recorrer la lista de elementos.

Nuestra aplicación de ejemplo se encarga de almacenar fotos en una base de datos, podríamos decir que nuestra compañía brinda el servicio de almacenamiento. Un cliente escoge cuáles fotos desea almacenar y nos envía la información usando una estructura de tipo ArrayList.

El método que se encarga de este proceso, sería de la siguiente manera.

//SavePhoto class

public void savePhotos(ArrayList<Photo> photos) {
    // take every photo inside the list and save it
    for(Photo photo : photos) {
        save(photo);
    }
}

private void save(Photo photo) {
    // save photo code
}

Por el momento, no hay ningún problema con este diseño. Sin embargo, ahora nos piden un cambio en la aplicación. A partir de ahora, podemos almacenar documentos en formato PDF.

Para estos documentos, el cliente decide enviarlos en un Array para optimizar un poco la memoria, ya que puede definir el número exacto de documentos que desea almacenar.

Estos serían los métodos dentro de nuestra clase SavePDF

//SavePDF class

public void savePDFs(PDFDoc[] pdfs) {
    // take every pdf inside the list and save it
    for(int i = 0; i < pdfs.length; i++) {
        save(pdf[i]);
    }
}

private void save(PDFDoc pdf) {
    // save photo code
}

¿Qué tienen en común los métodos savePhotos y savePDFs?

Ambos realizan la misma operación. Recorrer el contenedor y enviar cada elemento a otro método para almacenarlo. Estamos duplicando el código, lo cual, como hemos visto, puede introducir errores y es más difícil de mantener.

Además estamos programando en las implementaciones, no en las interfaces como aprendimos en otros patrones.

¿Qué pasa si nos piden almacenar facturas, y estas son enviadas en un HashMap, en donde la llave es el número de factura y el valor es la factura como tal?

Tendríamos que crear otra clase, con exactamente el mismo código, pero esta vez recorriendo los elementos del HashMap.

En resumen, este diseño no es flexible, no es escalable y no es mantenible. Este es el diagrama de clases actual.

Diagrama de clases del ejemplo

Empecemos a rediseñar un poco nuestra aplicación. Primero debemos extraer lo que es común en el ArrayList y en el Array. Las fotos y los PDF en realidad son documentos, ambos contienen un nombre, tipo (imagen, PDF, etc) y ambos son en realidad arreglos de bytes.

En lugar de tener múltiples tipos de documentos, podríamos crear un sólo tipo llamado MyDocument que contiene los atributos que son similares. Esto nos soluciona el problema de soportar futuros tipos de documentos.

Ahora pensemos en cambiar la forma en que recorremos los diferentes tipos de contenedores. Si recibimos un ArrayList, podemos utilizar el for resumido, pero si recibimos un arreglo, debemos utilizar arreglo[posición].

El principal problema es que la clase que se encarga de almacenar los documentos tiene total conocimiento de cómo se están implementando los contenedores de nuestros clientes. Es decir, no estamos encapsulando correctamente los detalles de nuestras clases.

Aquí es donde podemos utilizar el patrón Iterador. Podemos crear una interfaz que nos indique si el contenedor (ArrayList, List, Array, etc) contiene más elementos (hasNext) y además que nos facilite el siguiente elemento dentro del contenedor (next).

Luego, sólo debemos crear la implementación para los tipos de contenedores: ArrayListIterator y ArrayIterator.

Diagrama de clases del ejemplo utilizando patrón iterador

El nuevo método que se encarga de almacenar los documentos es el siguiente.

/**
* Handle the items to be saved regardless of their container implementation
* @param documents The iterable entity that contains all the items
*/
public void saveDocuments(Iterable documents) {
	// if there are more items
	while(documents.hasNext()) {
		// save the document
		save((MyDocument)documents.next());
	}
	System.out.println();
}

/**
 * Handle the save operation
 * @param document The document to save
 */
private void save(MyDocument document) {
	System.out.println(String.format("Saving the document: %s - type: %s", document.getName(), document.getType()));
}

El código anterior sólo contiene un ciclo, en el cual utilizamos la implementación de la interfaz Iterable. Ya no hay código duplicado y si necesitamos agregar un nuevo contenedor como un Set o un HashTable solamente es necesario implementar esta interfaz.

Esta es la definición del patrón Iterador en el libro Head First Design Patterns: Este patrón brinda una manera para acceder a los elementos de un contenedor, de forma secuencial, sin exponer su representación.

Este sería el diagrama de clases sugerido.

Diagrama de clases sugerido para el patrón iterador

Estas son algunas de las ventajas de utilizar este patrón:

  1. La clase que se encarga de almacenar los documentos no conoce si se esta usando un arreglo, un ArrayList o cualquier otro tipo de contenedor. Es decir, encapsulamos la definición de los contenedores.
  2. El nuevo diseño nos permite usar un único ciclo para almacenar los documentos.
  3. La clase que almacena los documentos usa una interfaz.

Este patrón nos introduce un nuevo principio de diseño, llamado Responsabilidad Única: una clase debe tener solo una razón para cambiar.

Si diseñamos nuestras clases de tal forma que, solo se encarguen de hacer una única tarea, estamos evitando posibles problemas en el futuro. Por ejemplo, si nos solicitan un cambio, sólo lo debemos aplicar en un lugar.

Por el contrario, si contamos con clases que tienen más de una responsabilidad, introducir cambios significa modificar el código en varios puntos, lo cual abre la puerta a errores dentro de nuestra aplicación.

El resultado de ejecutar el código de ejemplo es el siguiente.

Resultado de ejecutar el código de ejemplo

El código de ejemplo del patrón iterador lo pueden descargar aquí.

En el próximo post vamos a implementar Iterador pero usando la clase que nos ofrece Java.

Recordá suscribirte aquí para recibir las últimas actualizaciones todas las semanas.

Referencias:
Libro Head First Design Patterns. Versión digital.

Leave a Reply

Your email address will not be published. Required fields are marked *