← Volver al Inicio

Patrón Iterator

Recorre colecciones sin conocer su estructura interna

"Iterator permite recorrer elementos de una colección sin exponer su representación subyacente."

Recurso Audiovisual 📺 Video Explicativo del grupo.
CHAPTER 01

1. El Problema

Se está desarrollando el backend de un e-commerce. El catálogo de productos vive en un Árbol Binario para optimizar las búsquedas rápidas, mientras que las promociones se almacenan en un Array plano simple.

Cuando el carrito de compras o el módulo de facturación necesitan recorrer ambas colecciones para calcular un total, el código cliente termina lleno de bucles foreach para el array, combinados con una lógica de recursividad pesada y compleja para poder caminar a través del árbol binario.

¿Dónde está el dolor real? El cliente termina conociendo íntimamente cómo están guardados los datos por dentro. Si el día de mañana decides cambiar el árbol binario por una lista enlazada para mejorar el uso de memoria, romperás inmediatamente todo el código cliente del sistema que dependía de esa estructura.

CHAPTER 02

2. La Solución

El patrón Iterator extrae la responsabilidad del recorrido de la colección y se la delega a un objeto independiente: el Iterador. Este objeto encapsula por completo el estado de la iteración (en qué posición va y cuántos elementos quedan) y expone una interfaz pública unificada.

Gracias a esto, el cliente solo necesita pedirle el "siguiente" elemento al iterador, ignorando por completo si los datos provienen de un array, un árbol binario o cualquier otra estructura compleja.

Diagrama de la arquitectura del software

+---------------------+                      crea                     +-----------------------+
|  ProductCollection  |---------------------------------------------->|     ArrayIterator     |
+---------------------+                                               +-----------------------+
| - products: array   |                       +-------------------+   | - position: int       |
| + getIterator()     |                       |      Cliente      |   | + current()           |
+---------------------+                       | recorre con:      |   | + next(): void        |
           |                                  | hasNext() / next()|   | + hasNext(): bool     |
      implementa                              +-------------------+   +-----------------------+
           |                                     /             \                  |
           v                                  usa              usa           implementa
+---------------------+                     /                   \                 |
|   «interface»       |                    v                     v                v
+---------------------+          +---------------------+    +-------------------------+
|      Aggregate      |          |     «interface»     |    |       «interface»       |
+---------------------+          |      Aggregate      |    |        Iterator         |
| + getIterator()     |          +---------------------+    +-------------------------+
+---------------------+                                     | + current()             |
                                                            | + next(): void          |
                                                            | + hasNext(): bool       |
                                                            +-------------------------+
            

Estructura de Componentes

CHAPTER 03

3. Código Mínimo en TypeScript

A continuación se presenta el esqueleto en código aplicado a una Playlist musical, mostrando cómo el cliente recorre las canciones sin conocer el arreglo interno:

type Cancion = { titulo: string; artista: string };

interface IteradorCanciones {
    hasNext(): boolean;
    next(): Cancion | null;
}

interface ColeccionCanciones {
    crearIterador(): IteradorCanciones;
}

class Playlist implements ColeccionCanciones {
    private canciones: Cancion[] = [];

    public agregar(cancion: Cancion): void {
        this.canciones.push(cancion);
    }

    public crearIterador(): IteradorCanciones {
        return new PlaylistIterator(this.canciones);
    }
}

class PlaylistIterator implements IteradorCanciones {
    private posicion = 0;
    constructor(private readonly canciones: ReadonlyArray<Cancion>) {}

    public hasNext(): boolean {
        return this.posicion < this.canciones.length;
    }

    public next(): Cancion | null {
        if (!this.hasNext()) return null;
        return this.canciones[this.posicion++];
    }
}

const playlist = new Playlist();
playlist.agregar({ titulo: "Bohemian Rhapsody", artista: "Queen" });
playlist.agregar({ titulo: "Hotel California", artista: "Eagles" });

const iterador = playlist.crearIterador();
while (iterador.hasNext()) {
    const cancion = iterador.next()!;
    console.log(`Reproduciendo: ${cancion.titulo} - ${cancion.artista}`);
}
            

El detalle clave: La clase Playlist crea el iterador; el bloque while (que representa al cliente) únicamente utiliza las funciones hasNext() y next(), por lo que nunca conoce ni manipula el arreglo interno.

CHAPTER 04

4. Cuándo SÍ / Cuándo NO

Aplícalo cuando:

  • La estructura de datos subyacente es compleja (árboles binarios, grafos o sistemas con paginación).
  • Quieres aislar por completo la lógica de recorrido del módulo cliente (ej. reproducir una playlist musical).
  • Necesitas recorrer la misma colección de distintas formas (recorrido secuencial, aleatorio, filtrado, etc.).
  • Proteger y encapsular la estructura interna de la colección es una prioridad de arquitectura.

No lo apliques si:

  • Vas a procesar un elemento único y aislado (ej. reproducir o escuchar una nota de voz individual).
  • La colección es un arreglo plano o una lista simple nativa del lenguaje de programación.
  • Un bucle for o foreach nativo resuelve el problema de forma directa y sin ensuciar la lógica real.

El costo que siempre pagas: Más código y una mínima penalidad de rendimiento por la creación de objetos adicionales. Su uso solo se justifica si recorrer los datos dolía de verdad.

CHAPTER 05

5. Un Ejemplo Real en la Industria

Quiza lo has usado sin saberlo en el desarrollo de backend: las Lazy Collections de Laravel (bajo la clase Illuminate\Support\LazyCollection).

Este componente resuelve un problema real y crítico en entornos de producción: intentar cargar de golpe un millón de registros de una base de datos utilizando el ORM Eloquent colapsaría la memoria RAM del servidor al instantáneamente.

Laravel lo soluciona implementando el patrón Iterator apoyado en los generadores nativos de PHP (mediante la palabra clave yield). Esto permite escribir un bucle estándar en el controlador, pero manteniendo bajo el capó un único registro en memoria a la vez:

use App\Models\User;

$users = User::cursor(); 

foreach ($users as $user) {
    echo $user->email; 
}
            

¿Alguna vez utilizaste User::all() con una tabla masiva? El método User::cursor() existe justamente para evitar ese desbordamiento, permitiendo el mismo foreach de siempre mientras el patrón Iterator trabaja de forma invisible por debajo.