🚨 ¡Nueva review! ✨ Mi ratón favorito para programar: el Logitech MX Master 3S . ¡Échale un ojo! 👀

El Patrón de diseño Iterator

Recorre tus colecciones sin saber cómo están hechas por dentro.

Escrito por domin el 24 de noviembre de 2025 · Actualizado el 15 de febrero de 2026

🔄 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.

Diagrama del Patrón Iterator.

📺 1. Un ejemplo sencillo: El mando de la tele

Imagina que tienes una televisión (la colección):

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

Interfaz Iterator

Define las operaciones para recorrer: next(), hasMore(), current(), rewind(). Es el contrato que todo iterador debe cumplir.

Iterador Concreto

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.

Interfaz Aggregate (Colección)

Define un método para crear iteradores compatibles con la colección: getIterator(). Es la fábrica de iteradores.

Colección Concreta

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

ORMs (Doctrine, Eloquent)

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).

Generadores (yield)

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.

Paginación de APIs

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.

Recorrido de árboles

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ónPropósitoDiferencia clave
IteratorRecorrer una colección sin exponer su estructuraSe centra en el recorrido secuencial de los elementos
CompositeTratar objetos individuales y grupos uniformementeComposite define la estructura; Iterator la recorre
VisitorEjecutar operaciones sobre elementos de una estructuraVisitor opera sobre cada elemento; Iterator solo los entrega uno a uno
StrategyEncapsular un algoritmo y hacerlo intercambiableLos diferentes iteradores son como estrategias de recorrido
MementoGuardar y restaurar el estado de un objetoUn 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

SRP (Single Responsibility)

La colección se encarga de almacenar datos. El iterador se encarga de recorrerlos. Cada uno tiene un solo motivo para cambiar.

OCP (Open/Closed)

¿Quieres un nuevo recorrido? Creas un nuevo iterador (IteradorAleatorio). Sin tocar la colección ni los iteradores existentes.

DIP (Dependency Inversion)

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.

ISP (Interface Segregation)

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

Ventajas
  • 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.
Desventajas
  • Sobreingeniería: Si solo recorres un array simple, un foreach directo 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?

Úsalo cuando...
  • 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
Evítalo cuando...
  • 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! 🍻