22. Iterator (Iterador) - Patrón de Comportamiento

El patrón Iterator proporciona una forma uniforme de recorrer los elementos de una colección sin exponer su representación interna. Permite encapsular algoritmos de recorrido y ofrecer distintas vistas sobre estructuras complejas manteniendo un contrato sencillo para el cliente.

Es fundamental en bibliotecas de colecciones, motores de bases de datos en memoria y APIs que necesitan recorrer datos heterogéneos de manera consistente, fomentando el principio de responsabilidad única al separar la estructura del recorrido.

22.1 Problema y contexto

Las colecciones personalizadas suelen almacenar datos en estructuras complejas (listas enlazadas, grafos, tablas dispersas). Permitir que el cliente acceda directamente a los detalles internos viola el encapsulamiento y dificulta cambiar la implementación. Sin Iterator, cada consumidor debería conocer la estructura concreta para recorrer los elementos, generando acoplamiento fuerte y código duplicado.

El patrón resuelve el problema introduciendo un objeto iterador que mantiene el estado del recorrido. El cliente pide el siguiente elemento sin importar cómo esté organizada la colección subyacente, y la colección controla cuáles iteradores ofrece.

22.2 Intención y motivación

La intención es proporcionar una interfaz estándar para acceder secuencialmente a los elementos de un agregado sin exponer su representación. Es pertinente cuando:

  • Se desea soportar múltiples recorridos sobre la misma estructura (orden natural, orden filtrado, orden inverso).
  • La colección debe proteger su invariantes evitando que el cliente manipule directamente las referencias internas.
  • Se requiere que varios clientes recorran la misma colección en paralelo sin bloquearse mutuamente.
  • El diseño apunta a extender nuevos tipos de recorrido sin modificar el código cliente.

El patrón motiva a separar la responsabilidad de almacenar datos de la responsabilidad de recorrerlos, habilitando colecciones versátiles y fáciles de evolucionar.

22.3 Participantes y estructura

Los elementos principales son:

  • Iterator: interfaz que declara operaciones como TieneSiguiente() y Siguiente().
  • ConcreteIterator: implementaciones concretas que almacenan el estado del recorrido.
  • Aggregate: interfaz que declara un método para crear iteradores.
  • ConcreteAggregate: colecciones reales que devuelven iteradores específicos.
  • Cliente: utiliza el iterador sin conocer los detalles internos del agregado.

La estructura puede ampliarse con iteradores paralelos, iteradores seguros para concurrencia o iteradores que proyectan los datos a otro dominio.

22.4 Iteradores externos vs. internos

Existen dos enfoques clásicos:

  • Iterador externo: el cliente controla el avance llamando a next(). Facilita integrar el recorrido en bucles personalizados.
  • Iterador interno: la colección recibe una función y la ejecuta por cada elemento. Simplifica ciertos recorridos pero limita el control del cliente (por ejemplo, detenerse en medio).

Iterator se asocia principalmente al enfoque externo, aunque muchas bibliotecas combinan ambos estilos para ofrecer flexibilidad.

22.5 Escenario: recomendador de productos con filtros dinámicos

Un marketplace desea ofrecer un recorrido personalizado sobre su catálogo: según el perfil del cliente se necesitan filtros por categoría, disponibilidad e historial de compras. Además, se requiere exportar informes que recorran el catálogo en otro orden sin duplicar lógica.

Iterator permite encapsular estrategias de recorrido dentro de iteradores especializados. El catálogo expone métodos para crear iteradores filtrados o estadísticos; clientes distintos consumen la colección sin conocer cómo se almacenan los productos.

22.6 Implementación en C#

El siguiente ejemplo ilustra un catálogo que ofrece iteradores filtrados y la posibilidad de congelar la vista para recorridos consistentes, apoyándose en delegados Func<T, bool> y snapshots inmutables:

using System;
using System.Collections.Generic;

namespace Tutorial.Iterator
{
    public interface IIterador<out T>
    {
        bool TieneSiguiente();
        T Siguiente();
    }

    public sealed class IteradorFiltrado<T> : IIterador<T>
    {
        private readonly List<T> _elementos;
        private readonly Func<T, bool> _filtro;
        private int _indiceActual = -1;
        private int _indiceProximo = -1;
        private bool _tieneProximo;

        public IteradorFiltrado(IEnumerable<T> origen, Func<T, bool> filtro)
        {
            if (origen == null) throw new ArgumentNullException(nameof(origen));
            _filtro = filtro ?? throw new ArgumentNullException(nameof(filtro));
            _elementos = new List<T>(origen);
            PrepararProximo();
        }

        public bool TieneSiguiente() => _tieneProximo;

        public T Siguiente()
        {
            if (!_tieneProximo)
            {
                throw new InvalidOperationException("No hay mas elementos disponibles.");
            }

            _indiceActual = _indiceProximo;
            T actual = _elementos[_indiceActual];
            PrepararProximo();
            return actual;
        }

        private void PrepararProximo()
        {
            _tieneProximo = false;
            for (int i = _indiceActual + 1; i < _elementos.Count; i++)
            {
                T candidato = _elementos[i];
                if (_filtro(candidato))
                {
                    _indiceProximo = i;
                    _tieneProximo = true;
                    break;
                }
            }
        }
    }

    public sealed record Producto(string Sku, string Categoria, double Precio, bool Disponible)
    {
        public override string ToString() => $"{Sku} - {Categoria} - {Precio:C2} (disponible: {Disponible})";
    }

    public sealed class CatalogoProductos
    {
        private readonly List<Producto> _productos = new List<Producto>();

        public void Agregar(Producto producto)
        {
            if (producto == null) throw new ArgumentNullException(nameof(producto));
            _productos.Add(producto);
        }

        public IIterador<Producto> IteradorCompleto()
        {
            return new IteradorFiltrado<Producto>(_productos, _ => true);
        }

        public IIterador<Producto> IteradorFiltrado(Func<Producto, bool> filtro)
        {
            return new IteradorFiltrado<Producto>(_productos, filtro ?? (_ => true));
        }
    }

    public sealed class RecomendadorProductos
    {
        private readonly CatalogoProductos _catalogo;

        public RecomendadorProductos(CatalogoProductos catalogo)
        {
            _catalogo = catalogo ?? throw new ArgumentNullException(nameof(catalogo));
        }

        public void MostrarSugerencias(Func<Producto, bool> filtro)
        {
            IIterador<Producto> iterador = _catalogo.IteradorFiltrado(filtro);
            Console.WriteLine($"Sugerencias calculadas en {DateTimeOffset.UtcNow:O}");
            while (iterador.TieneSiguiente())
            {
                Console.WriteLine($" - {iterador.Siguiente()}");
            }
        }
    }

    public static class AplicacionIterator
    {
        public static void Main()
        {
            var catalogo = new CatalogoProductos();
            catalogo.Agregar(new Producto("SKU-100", "libros", 19.99, true));
            catalogo.Agregar(new Producto("SKU-200", "hogar", 89.50, true));
            catalogo.Agregar(new Producto("SKU-300", "libros", 24.90, false));
            catalogo.Agregar(new Producto("SKU-400", "tecnologia", 399.00, true));

            var recomendador = new RecomendadorProductos(catalogo);
            recomendador.MostrarSugerencias(producto =>
                producto.Categoria == "libros" && producto.Disponible);
        }
    }
}

22.7 Explicación del flujo

El catálogo conserva su representación interna privada y genera una copia inmutable para cada iterador, evitando efectos colaterales durante el recorrido. IteradorFiltrado calcula el siguiente elemento válido de forma perezosa con un delegado Func<Producto, bool>, lo que permite combinar filtros sin crear colecciones intermedias. El recomendador consume el iterador sin conocer la estructura interna y registra auditoría con DateTimeOffset.

22.8 Iterator en el ecosistema .NET

Las colecciones de .NET implementan IEnumerable<T>/IEnumerator<T>, que encapsulan el recorrido mediante MoveNext() y Current. El compilador genera iteradores personalizados a partir de yield return, y APIs como LINQ o IAsyncEnumerable<T> extienden el patrón para procesar datos de forma diferida, paralela o asíncrona conservando un contrato uniforme.

22.9 Variantes y extensiones

Iterator admite variantes según el dominio:

  • Iteradores bidireccionales: permiten avanzar y retroceder sobre la colección.
  • Iteradores con snapshot: capturan una vista inmutable para aislarse de modificaciones concurrentes.
  • Iteradores compuestos: recorren varias colecciones como si fueran una sola, muy utilizados en motores de consultas.
  • Iteradores perezosos: generan elementos bajo demanda, como ocurre en streams infinitos.

22.10 Riesgos y malas prácticas

Un riesgo frecuente es permitir que las modificaciones estructurales de la colección invalide iteradores activos sin notificación, provocando excepciones en tiempo de ejecución. También se debe evitar que los iteradores expongan detalles internos (referencias mutables) que los clientes podrían usar para alterar la colección sin pasar por las reglas de negocio.

Otra mala práctica es abusar de iteradores que realizan trabajo pesado en Siguiente(), degradando el rendimiento percibido y dificultando el control del tiempo de respuesta.

22.11 Buenas prácticas para aplicar Iterator

  • Definir contratos claros sobre concurrencia: especificar si el iterador es fail-fast, snapshot o tolerante.
  • Mantener la inmutabilidad de los elementos devueltos cuando se requiera preservar invariantes.
  • Ofrecer métodos auxiliares en la colección para crear iteradores especializados, evitando que el cliente componga filtros complejos por su cuenta.
  • Utilizar pruebas de rendimiento para validar que los iteradores personalizados no introducen sobrecarga innecesaria.

22.12 Relación con otros patrones

Iterator colabora con Composite para recorrer jerarquías de objetos sin exponer su estructura. Puede combinarse con Factory Method al crear iteradores especializados y con Strategy cuando el criterio de recorrido se selecciona dinámicamente. Además, suele trabajar junto a Memento para guardar la posición del recorrido y reanudarlo más tarde.