🔄 El Patrón Iterator: El Mando a Distancia Universal
El Patrón de diseño Iterator es un patrón de comportamiento y es probablemente el patrón que más usas sin darte cuenta. Cada vez que escribes un foreach en PHP, un for...of en JavaScript o un for...in en Python, estás usando un iterador.
Su objetivo es permitir recorrer los elementos de una colección sin exponer su estructura interna (lista, pila, árbol, hashmap…). El código cliente no sabe ni le importa si los datos están en un array, un árbol binario o una base de datos. Solo dice: “dame el siguiente”.
Extraes la lógica de recorrido a un objeto separado llamado Iterador, y tu colección solo se preocupa de almacenar datos.

📺 1. Un ejemplo sencillo: El mando de la tele
Imagina que tienes una televisión (la colección):
- La tele tiene canales guardados en su memoria interna (podría ser una lista, un array, un mapa de frecuencias… da igual).
- Tú usas el mando a distancia (el iterador).
- Pulsas “Siguiente” (
next()) o “Anterior”. No necesitas saber cómo la tele sintoniza la frecuencia o dónde guarda el número del canal. Solo sabes que si das a “Siguiente”, ves el siguiente canal.
Puedes tener varios mandos que recorran los canales de forma diferente. Uno en orden numérico, otro solo los canales favoritos, otro solo los de deportes. Cada mando es un iterador diferente sobre la misma tele.
¿Qué tipo de patrón de diseño es Iterator?
🛠️ 2. Los roles del patrón
Define las operaciones para recorrer: next(), hasMore(), current(), rewind(). Es el contrato que todo iterador debe cumplir.
Implementa la interfaz y mantiene el estado del recorrido: la posición actual, si ha terminado, etc. Cada iterador puede recorrer la colección de forma diferente.
Define un método para crear iteradores compatibles con la colección: getIterator(). Es la fábrica de iteradores.
Almacena los datos reales y devuelve una instancia del iterador concreto apropiado. La tele que guarda los canales y te da el mando.
El cliente solo conoce las interfaces. No sabe qué iterador concreto recibe ni cómo la colección almacena los datos. Solo llama a next() y hasMore().
¿Cuál es el principal problema que resuelve el patrón Iterator?
🧑💻 3. Ejemplo práctico en PHP: Colección con múltiples iteradores
Vamos a crear una colección de palabras que se puede recorrer de dos formas diferentes: en orden alfabético y en orden inverso. Misma colección, diferentes recorridos.
<?php
// 1. Interfaz Iterador
interface IteradorPersonalizado
{
public function tieneSiguiente(): bool;
public function siguiente(): string;
public function actual(): string;
public function reiniciar(): void;
}
// 2. Interfaz Colección (Aggregate)
interface ColeccionIterable
{
public function getIteradorAlfabetico(): IteradorPersonalizado;
public function getIteradorInverso(): IteradorPersonalizado;
}
// 3. Colección Concreta
class ColeccionPalabras implements ColeccionIterable
{
private array $items = [];
public function agregar(string $item): void
{
$this->items[] = $item;
}
public function getItems(): array
{
return $this->items;
}
public function count(): int
{
return count($this->items);
}
public function getIteradorAlfabetico(): IteradorPersonalizado
{
return new IteradorAlfabetico($this);
}
public function getIteradorInverso(): IteradorPersonalizado
{
return new IteradorInverso($this);
}
}
// 4. Iterador Concreto: Orden Alfabético
class IteradorAlfabetico implements IteradorPersonalizado
{
private int $posicion = 0;
private array $cacheOrdenada;
public function __construct(ColeccionPalabras $coleccion)
{
$this->cacheOrdenada = $coleccion->getItems();
sort($this->cacheOrdenada);
}
public function tieneSiguiente(): bool
{
return isset($this->cacheOrdenada[$this->posicion]);
}
public function siguiente(): string
{
$item = $this->cacheOrdenada[$this->posicion];
$this->posicion++;
return $item;
}
public function actual(): string
{
return $this->cacheOrdenada[$this->posicion];
}
public function reiniciar(): void
{
$this->posicion = 0;
}
}
// 5. Iterador Concreto: Orden Inverso
class IteradorInverso implements IteradorPersonalizado
{
private int $posicion;
private array $items;
public function __construct(ColeccionPalabras $coleccion)
{
$this->items = $coleccion->getItems();
$this->posicion = count($this->items) - 1;
}
public function tieneSiguiente(): bool
{
return $this->posicion >= 0;
}
public function siguiente(): string
{
$item = $this->items[$this->posicion];
$this->posicion--;
return $item;
}
public function actual(): string
{
return $this->items[$this->posicion];
}
public function reiniciar(): void
{
$this->posicion = count($this->items) - 1;
}
}
// 6. Uso: misma colección, diferentes recorridos
$lista = new ColeccionPalabras();
$lista->agregar("Zanahoria");
$lista->agregar("Arroz");
$lista->agregar("Manzana");
$lista->agregar("Banana");
$lista->agregar("Leche");
echo "--- Orden Alfabético ---\n";
$alfabetico = $lista->getIteradorAlfabetico();
while ($alfabetico->tieneSiguiente()) {
echo "• " . $alfabetico->siguiente() . "\n";
}
echo "\n--- Orden Inverso (como se añadieron) ---\n";
$inverso = $lista->getIteradorInverso();
while ($inverso->tieneSiguiente()) {
echo "• " . $inverso->siguiente() . "\n";
}
// Salida:
// --- Orden Alfabético ---
// • Arroz
// • Banana
// • Leche
// • Manzana
// • Zanahoria
//
// --- Orden Inverso (como se añadieron) ---
// • Leche
// • Banana
// • Manzana
// • Arroz
// • Zanahoria
La colección no cambia. Los datos están guardados de la misma forma. Lo que cambia es el iterador, que decide cómo recorrerlos. El código cliente ni se entera de cómo están almacenados por debajo.
¿Qué ventaja tiene poder tener múltiples iteradores sobre la misma colección?
🐘 4. Iterator en PHP nativo: la interfaz SPL
PHP trae de serie la interfaz Iterator en su SPL (Standard PHP Library). Si la implementas, tu colección funciona directamente con foreach. Nada de llamar a next() manualmente:
<?php
class ColeccionProductos implements \Iterator
{
private array $productos = [];
private int $posicion = 0;
public function agregar(string $producto): void
{
$this->productos[] = $producto;
}
// --- Métodos de la interfaz Iterator ---
public function current(): string
{
return $this->productos[$this->posicion];
}
public function key(): int
{
return $this->posicion;
}
public function next(): void
{
$this->posicion++;
}
public function rewind(): void
{
$this->posicion = 0;
}
public function valid(): bool
{
return isset($this->productos[$this->posicion]);
}
}
// Uso: funciona con foreach directamente
$productos = new ColeccionProductos();
$productos->agregar("Teclado");
$productos->agregar("Ratón");
$productos->agregar("Monitor");
foreach ($productos as $indice => $producto) {
echo "{$indice}: {$producto}\n";
}
// Salida:
// 0: Teclado
// 1: Ratón
// 2: Monitor
La interfaz Iterator de PHP define 5 métodos: current(), key(), next(), rewind() y valid(). En cuanto los
implementas, foreach sabe usarlos automáticamente.
También existe IteratorAggregate, que es aún más simple. En vez de implementar los 5 métodos, solo implementas getIterator() y devuelves un iterador existente:
class ColeccionSimple implements \IteratorAggregate
{
private array $items = [];
public function agregar(string $item): void
{
$this->items[] = $item;
}
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->items);
}
}
// Funciona igual con foreach
$coleccion = new ColeccionSimple();
$coleccion->agregar("Uno");
$coleccion->agregar("Dos");
foreach ($coleccion as $item) {
echo $item . "\n";
}
IteratorAggregate es ideal cuando no necesitas lógica personalizada de recorrido. Delegas a ArrayIterator y listo.
En PHP, ¿qué interfaz nativa permite que tu colección funcione directamente con foreach?
🎵 5. Ejemplo avanzado: Playlist con filtros
Vamos a ver un ejemplo más real. Una playlist de canciones que puedes recorrer de forma secuencial o filtrando por artista. Aquí se ve la potencia del patrón: el mismo código cliente funciona con cualquier iterador.
<?php
class Cancion
{
public function __construct(
public readonly string $titulo,
public readonly string $artista,
public readonly int $duracion // segundos
) {}
}
// Colección
class Playlist implements \IteratorAggregate
{
/** @var Cancion[] */
private array $canciones = [];
public function agregar(Cancion $cancion): void
{
$this->canciones[] = $cancion;
}
public function getCanciones(): array
{
return $this->canciones;
}
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->canciones);
}
public function porArtista(string $artista): IteradorPorArtista
{
return new IteradorPorArtista($this, $artista);
}
}
// Iterador filtrado por artista
class IteradorPorArtista implements \Iterator
{
private array $filtradas;
private int $posicion = 0;
public function __construct(Playlist $playlist, string $artista)
{
$this->filtradas = array_values(
array_filter(
$playlist->getCanciones(),
fn(Cancion $c) => strtolower($c->artista) === strtolower($artista)
)
);
}
public function current(): Cancion { return $this->filtradas[$this->posicion]; }
public function key(): int { return $this->posicion; }
public function next(): void { $this->posicion++; }
public function rewind(): void { $this->posicion = 0; }
public function valid(): bool { return isset($this->filtradas[$this->posicion]); }
}
// --- Función genérica que funciona con CUALQUIER iterador ---
function mostrarCanciones(iterable $canciones): void
{
foreach ($canciones as $cancion) {
$min = intdiv($cancion->duracion, 60);
$seg = str_pad($cancion->duracion % 60, 2, '0', STR_PAD_LEFT);
echo " 🎵 {$cancion->titulo} - {$cancion->artista} ({$min}:{$seg})\n";
}
}
// Uso
$playlist = new Playlist();
$playlist->agregar(new Cancion("Bohemian Rhapsody", "Queen", 354));
$playlist->agregar(new Cancion("Hotel California", "Eagles", 391));
$playlist->agregar(new Cancion("We Will Rock You", "Queen", 122));
$playlist->agregar(new Cancion("Stairway to Heaven", "Led Zeppelin", 482));
$playlist->agregar(new Cancion("Somebody to Love", "Queen", 296));
echo "--- Toda la playlist ---\n";
mostrarCanciones($playlist);
echo "\n--- Solo Queen ---\n";
mostrarCanciones($playlist->porArtista("Queen"));
// Salida:
// --- Toda la playlist ---
// 🎵 Bohemian Rhapsody - Queen (5:54)
// 🎵 Hotel California - Eagles (6:31)
// 🎵 We Will Rock You - Queen (2:02)
// 🎵 Stairway to Heaven - Led Zeppelin (8:02)
// 🎵 Somebody to Love - Queen (4:56)
//
// --- Solo Queen ---
// 🎵 Bohemian Rhapsody - Queen (5:54)
// 🎵 We Will Rock You - Queen (2:02)
// 🎵 Somebody to Love - Queen (4:56)
La función mostrarCanciones() no sabe si le están pasando toda la playlist o un subconjunto filtrado. Solo sabe que es
iterable. Esa es la gracia del Iterator: el consumidor y la colección están completamente desacoplados.
¿Cuál es la diferencia entre Iterator e IteratorAggregate en PHP?
🌍 6. Casos de uso en el mundo real
Cuando haces $users = User::all(), el ORM no carga 100.000 registros en memoria. Te devuelve un iterador que los va trayendo de la BD bajo demanda (lazy loading).
PHP tiene yield, que es un iterador nativo del lenguaje. Genera valores uno a uno sin cargar todo en memoria. Ideal para leer archivos CSV de millones de líneas.
Un iterador de API va pidiendo páginas bajo demanda: next() trae la siguiente página, hasMore() comprueba si quedan. El cliente solo recorre, no gestiona cursores.
Un árbol puede tener iteradores in-order, pre-order y post-order. El cliente no sabe nada de la estructura del árbol, solo llama a next().
🔄 7. Comparativa con otros patrones
| Patrón | Propósito | Diferencia clave |
|---|---|---|
| Iterator | Recorrer una colección sin exponer su estructura | Se centra en el recorrido secuencial de los elementos |
| Composite | Tratar objetos individuales y grupos uniformemente | Composite define la estructura; Iterator la recorre |
| Visitor | Ejecutar operaciones sobre elementos de una estructura | Visitor opera sobre cada elemento; Iterator solo los entrega uno a uno |
| Strategy | Encapsular un algoritmo y hacerlo intercambiable | Los diferentes iteradores son como estrategias de recorrido |
| Memento | Guardar y restaurar el estado de un objeto | Un iterador que soporta guardarPosicion() / restaurarPosicion() usa internamente la idea de Memento |
Un combo muy potente: Composite + Iterator. Puedes usar un Iterator para aplanar una estructura de árbol Composite y recorrerla como si fuera una lista lineal, sin que el cliente sepa nada de la recursividad.
⚖️ 8. Relación con SOLID
La colección se encarga de almacenar datos. El iterador se encarga de recorrerlos. Cada uno tiene un solo motivo para cambiar.
¿Quieres un nuevo recorrido? Creas un nuevo iterador (IteradorAleatorio). Sin tocar la colección ni los iteradores existentes.
El código cliente depende de la abstracción Iterator, no del iterador concreto ni de la estructura interna de la colección. Inversión perfecta.
La interfaz del iterador es mínima y enfocada: solo métodos de recorrido. No obliga a implementar nada que no necesites.
¿Qué principio SOLID se cumple al separar la lógica de recorrido de la colección?
✅ 9. Ventajas y desventajas
- Desacoplamiento total: El cliente no conoce la estructura interna de la colección.
- Múltiples recorridos: Puedes tener varios iteradores simultáneos sobre la misma colección.
- SRP: Separas la lógica de almacenamiento de la lógica de recorrido.
- Lazy evaluation: Puedes cargar elementos bajo demanda (yield, cursores de BD).
- OCP: Añadir nuevas formas de recorrer sin modificar la colección.
- Sobreingeniería: Si solo recorres un array simple, un
foreachdirecto es más que suficiente. - Overhead: Un objeto iterador extra por cada recorrido puede ser innecesario en colecciones sencillas.
- Estado compartido: Hay que tener cuidado con colecciones que cambian mientras se iteran.
- Complejidad: Para colecciones simples, añadir interfaces + iterador concreto + aggregate es matar moscas a cañonazos.
⚠️ 10. Errores comunes
1. Modificar la colección mientras iteras
El error clásico que destroza programas:
// ❌ MAL: eliminas un elemento y el índice se desplaza
$iterador = $coleccion->getIterator();
while ($iterador->tieneSiguiente()) {
$item = $iterador->siguiente();
if ($item === "Arroz") {
$coleccion->eliminar("Arroz"); // 💀 El iterador pierde la referencia
}
}
// ✅ BIEN: recoge los que quieres eliminar y bórralos después
$aEliminar = [];
$iterador = $coleccion->getIterator();
while ($iterador->tieneSiguiente()) {
$item = $iterador->siguiente();
if ($item === "Arroz") {
$aEliminar[] = $item;
}
}
foreach ($aEliminar as $item) {
$coleccion->eliminar($item);
}
¿Qué ocurre si modificas una colección mientras la estás iterando?
2. No implementar rewind() correctamente
Si rewind() no resetea el estado, el iterador se queda “gastado” y no puedes recorrer la colección dos veces:
// ❌ MAL: rewind no reinicia nada
public function rewind(): void
{
// Oops, se me olvidó
}
// ✅ BIEN: rewind deja el iterador como nuevo
public function rewind(): void
{
$this->posicion = 0;
}
3. Exponer la estructura interna desde el iterador
Si el iterador devuelve la referencia al array interno, el cliente puede modificarlo y romper la colección:
// ❌ MAL: devuelves la referencia al array original
public function getItems(): array
{
return &$this->items; // El cliente puede modificar los datos
}
// ✅ BIEN: devuelves una copia
public function getItems(): array
{
return $this->items; // PHP copia arrays por valor por defecto
}
En PHP los arrays se copian por valor, así que esto es seguro por defecto. Pero en lenguajes con paso por referencia (Java, C#), tienes que tener más cuidado y devolver una copia explícita.
4. Crear un iterador para un simple array
No todo necesita un iterador personalizado. Si solo tienes un array y lo recorres con foreach, no montes la infraestructura del patrón:
// ❌ Sobreingeniería: 50 líneas para recorrer un array
class MiColeccion implements \Iterator {
private array $datos = [1, 2, 3];
// ... 5 métodos para algo que se resuelve con foreach
}
// ✅ Sencillo y directo
$datos = [1, 2, 3];
foreach ($datos as $dato) {
echo $dato;
}
Usa el patrón cuando la colección tiene lógica compleja, múltiples formas de recorrido o necesitas lazy loading. No lo uses solo porque es un patrón.
🤔 11. ¿Cuándo usarlo y cuándo no?
- Tu colección tiene una estructura interna compleja (árbol, grafo, hashmap) que quieres ocultar
- Necesitas múltiples formas de recorrido sobre la misma colección
- Quieres soportar recorridos simultáneos (dos iteradores independientes sobre los mismos datos)
- Necesitas lazy loading: cargar elementos bajo demanda desde BD, API o archivos
- Quieres que el mismo código cliente funcione con diferentes tipos de colección
- Tu colección es un array simple que ya recorres con
foreach - Solo necesitas un tipo de recorrido (secuencial) y la estructura es trivial
- La colección cambia constantemente y no puedes garantizar consistencia durante el recorrido
- El acceso aleatorio es más importante que el secuencial (
$array[$i]es más directo)
Si estás a punto de exponer un
getArray()público para que el cliente recorra tus datos, para. Ese es el momento de usar un Iterator.
¿Cuándo NO deberías implementar el patrón Iterator?
💡 12. Conclusión
El Patrón Iterator es tan fundamental que la mayoría de lenguajes modernos lo tienen integrado: foreach en PHP, for...of en JavaScript, for...in en Python, Iterator en Java. Lo usas a diario sin darte cuenta.
Su poder real aparece cuando la colección no es un simple array: árboles, grafos, resultados paginados de API, millones de filas de BD… En esos casos, un iterador te permite recorrer todo de forma uniforme y bajo demanda, sin cargar todo en memoria.
Recuerda que la colección almacena, el iterador recorre. Cada uno a lo suyo. Si respetas esa separación, tu código será más limpio, más testeable y más extensible.
EA, ¡saluditos y nos vemos en los bares! 🍻