"Iterator permite recorrer elementos de una colección sin exponer su representación subyacente."
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.
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.
+---------------------+ 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 |
+-------------------------+
getIterator).current(), next() y hasNext().
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.
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.
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.