La carta de La Bella Napoli es un árbol Composite: secciones dentro de secciones, platos en las hojas, menús que agrupan. Al buscador de PideYa, sin embargo, el árbol le da igual: quiere "todos los platos, uno detrás de otro" para indexarlos. La pantalla de alérgenos quiere lo mismo; el comparador de precios, también. Y hoy cada uno reimplementa la recursión por su cuenta, conociendo SeccionCarta y sus hijos — la estructura interna de la carta, esparcida por media aplicación. Iterator resuelve exactamente esto: recorrer una colección sin exponer cómo está hecha por dentro. Es el patrón GoF que más usas sin saberlo — cada for-each que escribes es Iterator en acción — y esta lección lo destapa: de la estructura clásica al Iterable de Java, y de ahí a los Streams como su evolución natural.
Contenido
- El problema en PideYa: recorrer la carta sin conocerla
- Intención y estructura del patrón
- Iterator ya vive en Java:
Iterator,Iterabley for-each - Implementación: aplanar la carta Composite
- Segundo caso: historial de pedidos paginado
- Iteradores externos vs. internos, y Streams como evolución
- Cuándo usarlo y cuándo no
- Relación con otros patrones
- Errores comunes
- Ejercicios y conclusión
El problema en PideYa: recorrer la carta sin conocerla
Así indexa hoy el buscador los platos de una carta:
public class IndexadorBuscador {
public void indexar(ComponenteCarta componente) {
if (componente instanceof Plato plato) {
indice.añadir(plato);
} else if (componente instanceof SeccionCarta seccion) {
for (ComponenteCarta hijo : seccion.getHijos()) {
indexar(hijo); // recursión artesanal
}
} else if (componente instanceof Menu menu) {
// ...¿los platos de un menú se indexan? otro caso especial
}
}
}Y la pantalla de alérgenos tiene su propia copia de esta recursión, y el comparador de precios la suya. Los problemas:
- La estructura interna se ha filtrado a los clientes: todos saben que hay secciones con
getHijos(), todos haceninstanceof. Si mañanaSeccionCartaguarda a sus hijos en otro orden, o aparece un nuevo tipo de nodo, hay que cazar todas las copias de la recursión. - Recorrido duplicado: la lógica de "visitar todo el árbol" está escrita N veces, con N oportunidades de bugs distintos.
- Sin recorridos a medias: "dame los 10 primeros platos" obliga a recorrerlo todo o a ensuciar la recursión con contadores y banderas.
- Sin recorridos simultáneos: dos recorridos a la vez sobre la misma carta (búsqueda mientras se exporta) exigen que cada uno lleve su propio estado — cosa imposible si el estado del recorrido vive en la propia carta.
Lo que falta es un objeto cuya única responsabilidad sea el recorrido: saber por dónde va y cómo avanzar. Reificar el recorrido — el truco de la familia, una vez más.
Intención y estructura del patrón
Intención (GoF): proporcionar un modo de acceder secuencialmente a los elementos de un objeto agregado sin exponer su representación interna.
classDiagram
class IteradorPlatos {
<<interface>>
+tieneSiguiente() boolean
+siguiente() Plato
}
class Agregado {
<<interface>>
+crearIterador() IteradorPlatos
}
class SeccionCarta {
+crearIterador() IteradorPlatos
}
class IteradorProfundidadCarta {
-pendientes: Deque~ComponenteCarta~
+tieneSiguiente() boolean
+siguiente() Plato
}
class Cliente["IndexadorBuscador (cliente)"]
Agregado <|.. SeccionCarta
IteradorPlatos <|.. IteradorProfundidadCarta
SeccionCarta ..> IteradorProfundidadCarta : crea
Cliente --> IteradorPlatos : recorre con
Cliente --> Agregado : pide iterador a
| Rol GoF | En PideYa |
|---|---|
| Iterator (interfaz de recorrido) | IteradorPlatos (en Java real: Iterator<Plato>) |
| ConcreteIterator (sabe por dónde va) | IteradorProfundidadCarta |
| Aggregate (interfaz que fabrica iteradores) | Agregado (en Java real: Iterable<Plato>) |
| ConcreteAggregate | SeccionCarta |
Dos ideas capitales del diseño:
- El estado del recorrido vive en el iterador, no en la colección. Por eso puede haber varios recorridos simultáneos e independientes sobre la misma carta: cada uno es un objeto iterador distinto.
crearIterador()es un Factory Method: la colección concreta decide qué iterador concreto entrega, y el cliente solo ve la interfaz. GoF lo señala explícitamente: los dos patrones nacen juntos.
Iterator ya vive en Java: Iterator, Iterable y for-each
Antes de implementar nada, reconoce el patrón en el JDK, porque está calcado del GoF:
public interface Iterator<E> { // el rol Iterator
boolean hasNext();
E next();
default void remove() { throw new UnsupportedOperationException(); }
}
public interface Iterable<T> { // el rol Aggregate
Iterator<T> iterator(); // el factory method
}Y el for-each es azúcar sintáctico sobre el patrón. Estas dos versiones compilan a lo mismo:
for (Plato p : carta) { // lo que escribes
indice.añadir(p);
}
Iterator<Plato> it = carta.iterator(); // lo que ejecuta la JVM
while (it.hasNext()) {
Plato p = it.next();
indice.añadir(p);
}Es decir: cada vez que escribes un for-each sobre una List, un Set o un Map.entrySet(), estás usando Iterator — con la representación interna (array, nodos enlazados, tabla hash, árbol rojo-negro) perfectamente oculta tras hasNext()/next(). La moraleja práctica para PideYa: no inventes IteradorPlatos; implementa Iterable<Plato> y Iterator<Plato>, y todos tus recorridos ganan el for-each gratis.
Implementación: aplanar la carta Composite
Objetivo: que SeccionCarta sea Iterable<Plato> con un recorrido en profundidad que aplana secciones anidadas — el cliente ve un chorro de platos, el árbol desaparece.
La técnica clásica para recorrer un árbol sin recursión (la recursión no encaja en hasNext/next, que devuelven el control tras cada elemento) es una pila de trabajo pendiente:
public class IteradorProfundidadCarta implements Iterator<Plato> {
private final Deque<ComponenteCarta> pendientes = new ArrayDeque<>();
public IteradorProfundidadCarta(SeccionCarta raiz) {
pendientes.push(raiz);
avanzarHastaPlato();
}
/** Desapila hasta que la cima sea un Plato (o no quede nada). */
private void avanzarHastaPlato() {
while (!pendientes.isEmpty() && !(pendientes.peek() instanceof Plato)) {
ComponenteCarta actual = pendientes.pop();
if (actual instanceof SeccionCarta seccion) {
List<ComponenteCarta> hijos = seccion.getHijos();
for (int i = hijos.size() - 1; i >= 0; i--) {
pendientes.push(hijos.get(i)); // inverso: el 1º queda en la cima
}
}
// Menu u otros nodos sin platos propios: se descartan aquí
}
}
@Override
public boolean hasNext() {
return !pendientes.isEmpty();
}
@Override
public Plato next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Plato plato = (Plato) pendientes.pop();
avanzarHastaPlato(); // dejar preparado el siguiente
return plato;
}
}Puntos que merecen explicación:
- La pila sustituye a la pila de llamadas de la recursión: en vez de "función que se llama a sí misma", guardamos los nodos por visitar. Apilar los hijos en orden inverso hace que salgan en el orden natural de la carta.
- El invariante del iterador: entre llamadas, la cima de la pila es siempre el próximo plato (o la pila está vacía).
avanzarHastaPlato()restaura ese invariante tras cada extracción; asíhasNext()es trivial y barato. NoSuchElementExceptionennext()sin elementos: es el contrato estándar deIteratory los clientes (y el for-each) cuentan con él.
Ahora la colección entrega su iterador — y el instanceof desaparece de todos los clientes:
public class SeccionCarta extends ComponenteCarta implements Iterable<Plato> {
// ... todo lo que ya tenía del módulo 3 ...
@Override
public Iterator<Plato> iterator() {
return new IteradorProfundidadCarta(this);
}
}public class IndexadorBuscador {
public void indexar(SeccionCarta carta) {
for (Plato plato : carta) { // ni instanceof, ni getHijos(), ni recursión
indice.añadir(plato);
}
}
}¿Mañana la carta cambia su estructura interna, o queremos ofrecer un recorrido en anchura o "solo disponibles"? Se cambia (o se añade) un iterador; los clientes no se tocan.
Segundo caso: historial de pedidos paginado
"Mis pedidos" muestra el historial completo del cliente, pero el servidor lo entrega por páginas de 20. El cliente del código no debería saber nada de páginas — solo quiere "el siguiente pedido". El iterador es el lugar perfecto para esconder la paginación:
public class IteradorHistorialPedidos implements Iterator<Pedido> {
private final ServicioHistorial servicio;
private final String idCliente;
private List<Pedido> paginaActual = List.of();
private int posicionEnPagina = 0;
private int numeroPagina = 0;
private boolean ultimaPagina = false;
public IteradorHistorialPedidos(ServicioHistorial servicio, String idCliente) {
this.servicio = servicio;
this.idCliente = idCliente;
}
@Override
public boolean hasNext() {
if (posicionEnPagina < paginaActual.size()) {
return true;
}
if (ultimaPagina) {
return false;
}
paginaActual = servicio.pagina(idCliente, numeroPagina++, 20); // carga perezosa
posicionEnPagina = 0;
ultimaPagina = paginaActual.size() < 20;
return !paginaActual.isEmpty();
}
@Override
public Pedido next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return paginaActual.get(posicionEnPagina++);
}
}La elegancia está en lo que el cliente no ve: peticiones de red, tamaños de página, detección de la última. Y en lo que no pasa: si la UI solo muestra los 20 primeros, la página 2 jamás se pide — el iterador es perezoso por construcción, pariente del proxy virtual en espíritu: no hagas el trabajo hasta que lo pidan.
Iteradores externos vs. internos, y Streams como evolución
Todo lo anterior es iteración externa: el cliente controla el bucle — pide elemento a elemento con next(), y puede parar, intercalar o llevar dos iteradores a la vez. La alternativa es la iteración interna: le entregas a la colección la operación, y ella controla el bucle:
Externa (Iterator) |
Interna (forEach, Streams) |
|
|---|---|---|
| Quién controla el bucle | El cliente | La colección/el stream |
| Parar a medias, intercalar dos recorridos | Fácil | Difícil o imposible |
| Concisión, encadenar operaciones | Verboso | Excelente |
| Paralelizar | Manual | parallelStream() |
Los Streams de Java (Java 8+) son la iteración interna llevada a su madurez, y descienden directamente de este patrón: un Stream se construye sobre un Spliterator (un iterador con soporte de partición para paralelismo), es perezoso como nuestro iterador paginado, y esconde el recorrido tras operaciones declarativas:
List<String> sinGluten = StreamSupport.stream(carta.spliterator(), false)
.filter(Plato::esSinGluten)
.map(Plato::getNombre)
.sorted()
.toList();Fíjate en el dividendo: como hicimos SeccionCarta implements Iterable, los Streams nos salen gratis — carta.spliterator() viene de serie con Iterable. La regla práctica moderna: expón Iterable/Stream en tus colecciones de dominio; escribe un Iterator a mano solo cuando el recorrido en sí tiene lógica que encapsular (aplanar un árbol, paginar una API, mezclar fuentes).
Cuándo usarlo y cuándo no
Úsalo cuando:
- Los clientes recorren una estructura no trivial (árbol, páginas remotas, varias fuentes) y no deben conocer su representación.
- Quieres varios recorridos (orden natural, filtrado, en anchura) o varios simultáneos sobre la misma colección.
- Quieres recorridos perezosos sobre datos caros de obtener.
Evítalo cuando:
- La colección es una
Listnormal y el recorrido es de principio a fin: el for-each y los Streams del JDK ya son el patrón; escribir tu propioIteratorsería reimplementar el estándar. - El cliente necesita acceso posicional o aleatorio (
get(i)): el contrato secuencial del iterador estorba.
Coste real: los iteradores hechos a mano tienen su propia lógica delicada (invariantes, estado entre llamadas) y son un foco típico de errores off-by-one — escríbeles tests como a cualquier clase con estado.
Relación con otros patrones
- Composite: pareja natural — el composite define el árbol, el iterador lo recorre sin exponerlo. Con Visitor formarán el trío completo de "estructura + recorrido + operaciones".
- Factory Method:
iterator()es un factory method de manual. - Memento: GoF apunta que un iterador puede capturar su posición en un memento para retomarla.
- Proxy: el iterador paginado comparte espíritu con el proxy virtual (pereza ante recursos caros).
Errores comunes
- Modificar la colección mientras se itera: los iteradores del JDK son fail-fast y lanzan
ConcurrentModificationException. Para borrar durante el recorrido usaiterator.remove(),removeIf(...), o itera sobre una copia. hasNext()con efectos que se repiten: los clientes pueden llamar ahasNext()varias veces seguidas; debe ser idempotente (nuestro paginado carga página solo cuando se agota la actual, la llamen las veces que la llamen).- Guardar el estado del recorrido en la colección ("un cursor interno"): mata los recorridos simultáneos y deja la colección en estado inconsistente si un recorrido se abandona a medias.
- Devolver
nullcomo fin de recorrido en vez dehasNext() == false+NoSuchElementException: rompe el contrato que el for-each y todo el ecosistema esperan. - Iteradores que exponen lo que venían a ocultar: si tu
Iterator<ComponenteCarta>obliga al cliente a hacerinstanceofcon lo que recibe, el problema sigue ahí; elige bien el tipo de elemento (nosotros devolvemosPlato, noComponenteCarta).
Ejercicios
Ejercicio 1: iterador filtrado
Implementa IteradorDisponibles, que envuelve un Iterator<Plato> cualquiera y solo entrega platos con estaDisponible() == true. (Pista: es un iterador-decorador; necesitarás "mirar por adelantado" el próximo válido para poder responder hasNext().)
Ejercicio 2: for-each sobre el historial
Haz que el historial se pueda usar con for-each: escribe HistorialPedidos implements Iterable<Pedido> apoyándote en IteradorHistorialPedidos. ¿Por qué es importante que iterator() devuelva un iterador nuevo en cada llamada?
Ejercicio 3: ¿iterador a mano o Stream?
Para cada caso, di si escribirías un Iterator propio o usarías Streams/for-each sobre lo existente: (a) sumar los precios de una List<LineaPedido>; (b) recorrer los pedidos de hoy mezclando dos fuentes ordenadas (app y teléfono) en orden cronológico global; (c) los 5 platos más caros de la carta.
Soluciones
Solución 1:
public class IteradorDisponibles implements Iterator<Plato> {
private final Iterator<Plato> origen;
private Plato proximo; // el siguiente válido, ya localizado
public IteradorDisponibles(Iterator<Plato> origen) {
this.origen = origen;
avanzar();
}
private void avanzar() {
proximo = null;
while (origen.hasNext()) {
Plato candidato = origen.next();
if (candidato.estaDisponible()) {
proximo = candidato;
return;
}
}
}
@Override public boolean hasNext() { return proximo != null; }
@Override public Plato next() {
if (proximo == null) throw new NoSuchElementException();
Plato resultado = proximo;
avanzar();
return resultado;
}
}El "mirar por adelantado" es obligatorio: sin localizar el próximo válido, hasNext() no podría responder con verdad. Y sí, es la mecánica de Decorator aplicada a iteradores — misma interfaz, envuelve, filtra.
Solución 2:
public class HistorialPedidos implements Iterable<Pedido> {
private final ServicioHistorial servicio;
private final String idCliente;
public HistorialPedidos(ServicioHistorial servicio, String idCliente) {
this.servicio = servicio;
this.idCliente = idCliente;
}
@Override
public Iterator<Pedido> iterator() {
return new IteradorHistorialPedidos(servicio, idCliente); // ¡nuevo cada vez!
}
}Debe ser nuevo en cada llamada porque el estado del recorrido vive en el iterador: si iterator() devolviera siempre el mismo, el segundo for-each empezaría donde acabó el primero (o dos recorridos simultáneos se pisarían las páginas).
Solución 3: (a) Stream de una línea: lineas.stream().map(LineaPedido::getPrecio).reduce(ZERO, BigDecimal::add) — nada que encapsular; (b) iterador a mano: la mezcla ordenada de dos fuentes es lógica de recorrido genuina (comparar cabezas, avanzar la menor) que merece su clase y sus tests; (c) Stream sobre el Iterable que ya tenemos: stream(carta.spliterator(), false).sorted(comparing(Plato::getPrecio).reversed()).limit(5).
Conclusión
Iterator reifica el recorrido: la posición y la lógica de avance viven en un objeto con dos métodos, la estructura interna queda sellada, y varios recorridos —completos, filtrados, perezosos, simultáneos— conviven sin tocarse. En PideYa, la carta Composite se aplana tras Iterable<Plato> (adiós a las recursiones duplicadas con instanceof) y el historial esconde su paginación de red tras un hasNext(). Y has visto la genealogía completa: del GoF al Iterator/Iterable del JDK, del for-each como azúcar sintáctico a los Streams como iteración interna, perezosa y paralelizable.
Hasta aquí, patrones que organizan el diálogo entre dos partes: cliente y colección, invocador y comando. El siguiente ataca las conversaciones multitudinarias. En el reparto de PideYa, los pedidos listos, los repartidores disponibles y la cocina se llaman unos a otros en una malla donde todos conocen a todos — y cada pieza nueva multiplica las conexiones. La solución: que nadie hable con nadie... salvo a través de uno. Nos vemos en Mediator.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
