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.
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.
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:
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.
Los elementos principales son:
TieneSiguiente()
y Siguiente()
.La estructura puede ampliarse con iteradores paralelos, iteradores seguros para concurrencia o iteradores que proyectan los datos a otro dominio.
Existen dos enfoques clásicos:
next()
. Facilita integrar el recorrido en bucles personalizados.Iterator se asocia principalmente al enfoque externo, aunque muchas bibliotecas combinan ambos estilos para ofrecer flexibilidad.
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.
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);
}
}
}
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
.
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.
Iterator admite variantes según el dominio:
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.
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.