patron-iterator

Patrón Iterator: recorre colecciones

  • 5 min

Un Iterator es un objeto que recorre una colección sin exponer su representación interna. Separa el avance por los elementos de la estructura que los almacena.

Imaginad que sois turistas en una ciudad enorme como Roma. Queréis ver todas las atracciones turísticas.

Podéis hacerlo de varias formas:

  1. A lo loco: Caminar aleatoriamente hasta encontrar algo.
  2. Secuencialmente: Seguir una guía paso a paso de una revista.
  3. Con guía turístico: Contratáis a alguien que conoce la ciudad y os lleva de un sitio a otro.

El iterador cumple el papel de guía del recorrido.

En programación, tenemos muchas formas de almacenar grupos de objetos: Arrays, Listas Enlazadas, Árboles Binarios, Grafos, HashMaps… Cada una tiene una estructura interna diferente.

  • Un Array se recorre con un índice i++.
  • Una Lista Enlazada se recorre saltando de nodo en nodo.
  • Un Árbol se recorre en pre-orden, in-orden o post-orden.

El problema es que, si nuestro código cliente (el que usa la colección) tiene que saber cómo está estructurada la colección para recorrerla, estamos creando un acoplamiento brutal. Si mañana cambiáis la lista por un árbol, tendréis que reescribir todos los bucles for de vuestra aplicación.

El Problema: Acceso a elementos

Queremos una forma de acceder a los elementos de un contenedor secuencialmente sin exponer su representación interna.

No queremos que el cliente haga esto:

// ❌ El cliente sabe demasiado sobre la estructura interna
for (int i = 0; i < coleccion.Length; i++) { ... }
// o
while (nodo != null) { nodo = nodo.Siguiente; }
Copied!

Queremos que haga esto:

// ✅ El cliente solo pide "el siguiente"
while (iterador.TieneSiguiente()) {
    var elemento = iterador.Siguiente();
}
Copied!

La Solución: El Iterator

El patrón sugiere extraer la lógica de recorrido de la colección y ponerla en un objeto separado llamado Iterador.

Implementación “Didáctica” en C#

Aunque C# ya tiene esto resuelto (luego lo vemos), vamos a implementarlo desde cero para entender la mecánica.

Las Interfaces

// Interfaz del Iterador
public interface IIterador<T>
{
    T Siguiente();
    bool TieneSiguiente();
}

// Interfaz de la Colección (Agregado)
public interface IColeccion<T>
{
    IIterador<T> CrearIterador();
}
Copied!

La Colección Concreta

Imagina una Playlist de Spotify. Internamente usa una lista, pero podría usar cualquier cosa.

public class Playlist : IColeccion<string>
{
    private List<string> _canciones = new List<string>();

    public void AgregarCancion(string cancion) => _canciones.Add(cancion);

    public int Count => _canciones.Count;
    public string Get(int index) => _canciones[index];

    // Fábrica de iteradores
    public IIterador<string> CrearIterador()
    {
        return new PlaylistIterator(this);
    }
}
Copied!

El Iterador Concreto

Este es el objeto que sabe cómo moverse por la Playlist.

public class PlaylistIterator : IIterador<string>
{
    private Playlist _playlist;
    private int _posicionActual = 0;

    public PlaylistIterator(Playlist playlist)
    {
        _playlist = playlist;
    }

    public bool TieneSiguiente()
    {
        return _posicionActual < _playlist.Count;
    }

    public string Siguiente()
    {
        if (!TieneSiguiente()) return null;
        
        string cancion = _playlist.Get(_posicionActual);
        _posicionActual++;
        return cancion;
    }
}
Copied!

El Cliente

class Program
{
    static void Main(string[] args)
    {
        var miMusica = new Playlist();
        miMusica.AgregarCancion("Bohemian Rhapsody");
        miMusica.AgregarCancion("Stairway to Heaven");
        miMusica.AgregarCancion("Hotel California");

        // El cliente pide un iterador. 
        // NO sabe si por dentro hay un array o una lista enlazada.
        IIterador<string> iterador = miMusica.CrearIterador();

        Console.WriteLine("--- Reproduciendo Playlist ---");
        while (iterador.TieneSiguiente())
        {
            string cancion = iterador.Siguiente();
            Console.WriteLine($"🎵 Sonando: {cancion}");
        }
    }
}
Copied!

Implementación Real en C# (IEnumerable y yield)

En .NET, el patrón Iterator está tan integrado que el lenguaje tiene soporte nativo.

  • La interfaz Aggregate es IEnumerable<T>.
  • La interfaz Iterator es IEnumerator<T>.

Y lo mejor de todo: no necesitamos crear la clase PlaylistIterator manualmente. C# nos da la palabra clave yield return.

Cuando usamos yield return, el compilador genera automáticamente por detrás una clase que implementa el patrón Iterator (una máquina de estados).

El Ejemplo Moderno

// Implementamos IEnumerable para que C# sepa que esto se puede recorrer
public class PlaylistModerna : IEnumerable<string>
{
    private List<string> _canciones = new List<string>();

    public void Add(string cancion) => _canciones.Add(cancion);

    // Implementación del iterador
    public IEnumerator<string> GetEnumerator()
    {
        // Podríamos hacer lógica compleja aquí, como recorrer al revés
        // o filtrar canciones explícitas.
        foreach (var c in _canciones)
        {
            // yield return devuelve el elemento y "pausa" la ejecución
            // hasta que el bucle pida el siguiente.
            yield return c; 
        }
    }

    // Boilerplate necesario por compatibilidad con versiones antiguas
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Copied!

Uso con foreach

El bucle foreach es simplemente azúcar sintáctico. El compilador lo traduce a llamadas while (iterador.MoveNext()).

var playlist = new PlaylistModerna();
playlist.Add("Despacito"); // (Lo siento)
playlist.Add("Gasolina");

// Esto usa el patrón Iterator por debajo
foreach (var cancion in playlist)
{
    Console.WriteLine(cancion);
}
Copied!

¿Por qué es útil esto?

El poder real del Iterator no es solo recorrer listas. Es abstraer recorridos complejos.

Imagina un Árbol Genealógico. Podéis crear iteradores diferentes para la misma colección:

  • GetAntepasadosIterator(): Recorre hacia arriba (padres, abuelos…).
  • GetDescendientesIterator(): Recorre hacia abajo.
  • GetHijosSoloVaronesIterator(): Recorre filtrando.

El código cliente sigue siendo un simple foreach, pero la lógica de “cómo me muevo por el árbol” está encapsulada en cada iterador.

Hoy en día, gracias a IEnumerable y yield en C#, o los Generadores en JavaScript/Python, implementarlo es trivial, pero entender lo que ocurre detrás nos ayuda a diseñar estructuras de datos más robustas.