Design Patterns - Iterator

Propósito

El patrón Iterator (Iterador) brinda una manera de acceder a los elementos de un objeto agregado de forma secuencial, sin exponer su representación interna, el cliente no necesita saber cómo es su estructura.

Problema

El problema que resuelve es la necesidad de acceder a los elementos de una colección de manera uniforme, independientemente de cómo esté estructurada internamente la colección (array, lista, árbol, etc.).

O sea, encontrar una manera de recorrer cualquier tipo de colección (agregado) sin importar su estructura.

Solución

El patrón Iterator soluciona este problema mediante un objeto iterador, que encapsula el acceso y la navegación por la colección.

El iterador tiene métodos para acceder a los elementos de la colección, de uno en uno, sin necesidad de entender su estructura interna.

Esta solución permite que un mismo método de recorrido pueda ser utilizado para diferentes tipos de colecciones, y además separa la lógica de recorrido de la lógica de negocio.

Estructura

iterator

Participantes

  • Iterator: Define una interfaz para recorrer los elementos y acceder a ellos.
  • ConcreteIterator: Implementa la interfaz de Iterator y mantiene la posición actual del recorrido.
  • Aggregate: Define una interfaz para crear un objeto Iterator.
  • ConcreteAggregate: Implementa la interfaz Aggregate y retorna una instancia de ConcreteIterator correspondiente.
  • Client: Mediante el Iterator recorre el Aggregate.

Cuándo Usarlo

Este patrón es recomendable cuando:

  • se quiere acceder a los elementos de una colección sin exponer su representación interna.
  • hay múltiples formas de recorrer una colección y se quiere encapsular cada una de ellas.

Ventajas

Separación de Responsabilidades: Separa la lógica de acceso y recorrido de una colección, de su lógica de negocio.

Flexibilidad: Permite diferentes formas de recorrer una colección, facilitando incluso la implementación de iteradores para recorridos complejos.

Acceso Uniforme a Colecciones: Proporciona un acceso uniforme a los elementos de diferentes tipos de colecciones.

Desventajas

Complejidad Adicional: Agrega una capa adicional de complejidad.

Sobrecarga: Puede introducir una sobrecarga en comparación con el acceso directo a los elementos de la colección.

Inconsistencia Potencial: Si la colección se modifica durante la iteración, puede dar lugar a inconsistencias o errores.

Ejemplo: Libro de Recetas Digital

Debemos crear un libro de recetas digital que permite a los usuarios ver y seguir recetas paso a paso.

Cada receta está compuesta por una serie de pasos que deben seguirse en un orden específico para completar el plato.

Problema

Debemos navegar a través de los diversos pasos de cada receta, de una forma eficiente y flexible.

Los usuarios deben poder avanzar hacia el próximo paso, retroceder si es necesario, y tal vez saltar a pasos específicos sin confusión.

Implementar esta funcionalidad directamente en la colección de pasos puede llevar a un diseño rígido y difícil de mantener, especialmente a medida que agregamos más características como diferentes modos de navegación.

Solución planteada

El patrón Iterator es ideal para este escenario porque nos permite recorrer los pasos de cada receta sin exponer los detalles internos de cómo se almacenan y mantienen.

Esto nos da la flexibilidad para cambiar la implementación subyacente de la colección de pasos sin afectar la forma en que los usuarios interactúan con ellos.

Además, facilita la extensión del sistema para incluir diferentes maneras de iterar a través de los pasos en el futuro.

Creamos las interfaces Iterator y Aggregate. Implementamos Receta como un agregado que tiene una lista de pasos, y el iterador de la receta. Finalmente, el Libro de cocina usa el iterador para recorrer la Receta.

iterator

Código Java

Codificamos en Java lo que preparamos en el diagrama.

Creamos la Interfaz Iterator:


              
  interface Iterator {
      boolean hasNext();
      Object next();
  }
  
              

Creamos la Interfaz Aggregate:


              
  interface Aggregate {
      Iterator createIterator();
  }
  
              

Implementamos el ConcreteAggregate:


              
  class Recipe implements Aggregate {
      private String[] steps;
  
      public Recipe(String[] steps) {
          this.steps = steps;
      }
  
      @Override
      public Iterator createIterator() {
          return new RecipeIterator(steps);
      }
  }
              
              

Implementamos el ConcreteIterator:


              
  class RecipeIterator implements Iterator {
      private String[] steps;
      private int position = 0;
  
      public RecipeIterator(String[] steps) {
          this.steps = steps;
      }
  
      @Override
      public boolean hasNext() {
          return position < steps.length;
      }
  
      @Override
      public Object next() {
          if (this.hasNext()) {
              return steps[position++];
          }
          return null;
      }
  }
              
              

El código cliente:


              
  public class CookbookApp {
      public static void main(String[] args) {
          String[] steps = {
              "Precalentar el horno", 
              "Mezclar ingredientes", 
              "Hornee a fuego alto por 20 minutos"
          };
          Recipe recipe = new Recipe(steps);
          Iterator iterator = recipe.createIterator();
  
          while (iterator.hasNext()) {
              String step = (String) iterator.next();
              System.out.println(step);
          }
      }
  }
              
              

Mapeo (del ejemplo a Participantes)

Los participantes que vimos antes son: Iterator, Aggregate, ConcreteAggregate, ConcreteIterator, Client:

  • Iterator (Iterator): Interface para métodos hasNext y next.
  • Aggregate (Aggregate): Interface para crear un Iterator.
  • Recipe (ConcreteAggregate): Colección de pasos en una receta que puede ser recorrida.
  • RecipeIterator (ConcreteIterator): Implementa la navegación paso a paso para una receta.
  • CookbookApp (Client): Utiliza el Iterator para recorrer y mostrar los pasos de la receta.

Conclusiones

Este ejemplo muestra cómo usar el patrón Iterator en el recorrido de una colección. Al aplicarlo logramos flexibilidad, ya que podemos cambiar la colección sin que afecte el recorrido, y facilidad de extensión, ya que podemos incluir distintas formas de recorrerlos.

Patrones relacionados

  • Composite

Se puede combinar con Iterator para recorrer estructuras compuestas.

  • Factory Method

Puede ser usado para crear el iterador concreto en el agregado.