La carta de La Bella Napoli és un arbre Composite: seccions dins de seccions, plats a les fulles, menús que agrupen. Al cercador de PideYa, però, l'arbre tant li fa: vol "tots els plats, l'un darrere l'altre" per indexar-los. La pantalla d'al·lèrgens vol el mateix; el comparador de preus, també. I avui cadascun reimplementa la recursió pel seu compte, coneixent SeccioCarta i els seus fills — l'estructura interna de la carta, escampada per mitja aplicació. Iterator resol exactament això: recórrer una col·lecció sense exposar com està feta per dins. És el patró GoF que més uses sense saber-ho — cada for-each que escrius és Iterator en acció — i aquesta lliçó el destapa: de l'estructura clàssica a l'Iterable de Java, i d'allà als Streams com la seva evolució natural.

Contingut

  1. El problema a PideYa: recórrer la carta sense conèixer-la
  2. Intenció i estructura del patró
  3. Iterator ja viu a Java: Iterator, Iterable i for-each
  4. Implementació: aplanar la carta Composite
  5. Segon cas: historial de comandes paginat
  6. Iteradors externs vs. interns, i Streams com a evolució
  7. Quan usar-lo i quan no
  8. Relació amb altres patrons
  9. Errors comuns
  10. Exercicis i conclusió

El problema a PideYa: recórrer la carta sense conèixer-la

Així indexa avui el cercador els plats d'una carta:

public class IndexadorCercador {

    public void indexar(ComponentCarta component) {
        if (component instanceof Plat plat) {
            index.afegir(plat);
        } else if (component instanceof SeccioCarta seccio) {
            for (ComponentCarta fill : seccio.getFills()) {
                indexar(fill);                    // recursio artesanal
            }
        } else if (component instanceof Menu menu) {
            // ...els plats d'un menu s'indexen? un altre cas especial
        }
    }
}

I la pantalla d'al·lèrgens té la seva pròpia còpia d'aquesta recursió, i el comparador de preus la seva. Els problemes:

  • L'estructura interna s'ha filtrat als clients: tots saben que hi ha seccions amb getFills(), tots fan instanceof. Si demà SeccioCarta guarda els fills en un altre ordre, o apareix un tipus nou de node, cal caçar totes les còpies de la recursió.
  • Recorregut duplicat: la lògica de "visitar tot l'arbre" està escrita N vegades, amb N oportunitats de bugs diferents.
  • Sense recorreguts a mitges: "dona'm els 10 primers plats" obliga a recórrer-ho tot o a embrutar la recursió amb comptadors i banderes.
  • Sense recorreguts simultanis: dos recorreguts alhora sobre la mateixa carta (cerca mentre s'exporta) exigeixen que cadascun porti el seu propi estat — cosa impossible si l'estat del recorregut viu a la carta mateixa.

El que falta és un objecte l'única responsabilitat del qual sigui el recorregut: saber per on va i com avançar. Reïficar el recorregut — el truc de la família, un cop més.

Intenció i estructura del patró

Intenció (GoF): proporcionar una manera d'accedir seqüencialment als elements d'un objecte agregat sense exposar-ne la representació interna.

classDiagram
    class IteradorPlats {
        <<interface>>
        +teSeguent() boolean
        +seguent() Plat
    }
    class Agregat {
        <<interface>>
        +crearIterador() IteradorPlats
    }
    class SeccioCarta {
        +crearIterador() IteradorPlats
    }
    class IteradorProfunditatCarta {
        -pendents: Deque~ComponentCarta~
        +teSeguent() boolean
        +seguent() Plat
    }
    class Client["IndexadorCercador (client)"]

    Agregat <|.. SeccioCarta
    IteradorPlats <|.. IteradorProfunditatCarta
    SeccioCarta ..> IteradorProfunditatCarta : crea
    Client --> IteradorPlats : recorre amb
    Client --> Agregat : demana iterador a
Rol GoF A PideYa
Iterator (interfície de recorregut) IteradorPlats (en Java real: Iterator<Plat>)
ConcreteIterator (sap per on va) IteradorProfunditatCarta
Aggregate (interfície que fabrica iteradors) Agregat (en Java real: Iterable<Plat>)
ConcreteAggregate SeccioCarta

Dues idees capitals del disseny:

  • L'estat del recorregut viu a l'iterador, no a la col·lecció. Per això hi pot haver diversos recorreguts simultanis i independents sobre la mateixa carta: cadascun és un objecte iterador diferent.
  • crearIterador() és un Factory Method: la col·lecció concreta decideix quin iterador concret lliura, i el client només veu la interfície. GoF ho assenyala explícitament: els dos patrons neixen junts.

Iterator ja viu a Java: Iterator, Iterable i for-each

Abans d'implementar res, reconeix el patró al JDK, perquè està calcat 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
}

I el for-each és sucre sintàctic sobre el patró. Aquestes dues versions compilen al mateix:

for (Plat p : carta) {                       // el que escrius
    index.afegir(p);
}

Iterator<Plat> it = carta.iterator();        // el que executa la JVM
while (it.hasNext()) {
    Plat p = it.next();
    index.afegir(p);
}

És a dir: cada cop que escrius un for-each sobre una List, un Set o un Map.entrySet(), estàs usant Iterator — amb la representació interna (array, nodes enllaçats, taula hash, arbre roig-negre) perfectament oculta rere hasNext()/next(). La moralitat pràctica per a PideYa: no inventis IteradorPlats; implementa Iterable<Plat> i Iterator<Plat>, i tots els teus recorreguts guanyen el for-each gratis.

Implementació: aplanar la carta Composite

Objectiu: que SeccioCarta sigui Iterable<Plat> amb un recorregut en profunditat que aplana seccions imbricades — el client veu un doll de plats, l'arbre desapareix.

La tècnica clàssica per recórrer un arbre sense recursió (la recursió no encaixa a hasNext/next, que retornen el control després de cada element) és una pila de feina pendent:

public class IteradorProfunditatCarta implements Iterator<Plat> {

    private final Deque<ComponentCarta> pendents = new ArrayDeque<>();

    public IteradorProfunditatCarta(SeccioCarta arrel) {
        pendents.push(arrel);
        avancarFinsAPlat();
    }

    /** Desapila fins que el cim sigui un Plat (o no quedi res). */
    private void avancarFinsAPlat() {
        while (!pendents.isEmpty() && !(pendents.peek() instanceof Plat)) {
            ComponentCarta actual = pendents.pop();
            if (actual instanceof SeccioCarta seccio) {
                List<ComponentCarta> fills = seccio.getFills();
                for (int i = fills.size() - 1; i >= 0; i--) {
                    pendents.push(fills.get(i));   // invers: el 1r queda al cim
                }
            }
            // Menu o altres nodes sense plats propis: es descarten aqui
        }
    }

    @Override
    public boolean hasNext() {
        return !pendents.isEmpty();
    }

    @Override
    public Plat next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        Plat plat = (Plat) pendents.pop();
        avancarFinsAPlat();                        // deixar preparat el seguent
        return plat;
    }
}

Punts que mereixen explicació:

  • La pila substitueix la pila de crides de la recursió: en comptes de "funció que es crida a si mateixa", guardem els nodes per visitar. Apilar els fills en ordre invers fa que surtin en l'ordre natural de la carta.
  • L'invariant de l'iterador: entre crides, el cim de la pila és sempre el proper plat (o la pila és buida). avancarFinsAPlat() restaura aquest invariant després de cada extracció; així hasNext() és trivial i barat.
  • NoSuchElementException a next() sense elements: és el contracte estàndard d'Iterator i els clients (i el for-each) hi compten.

Ara la col·lecció lliura el seu iterador — i l'instanceof desapareix de tots els clients:

public class SeccioCarta extends ComponentCarta implements Iterable<Plat> {
    // ... tot el que ja tenia del modul 3 ...

    @Override
    public Iterator<Plat> iterator() {
        return new IteradorProfunditatCarta(this);
    }
}
public class IndexadorCercador {
    public void indexar(SeccioCarta carta) {
        for (Plat plat : carta) {            // ni instanceof, ni getFills(), ni recursio
            index.afegir(plat);
        }
    }
}

Demà la carta canvia la seva estructura interna, o volem oferir un recorregut en amplada o "només disponibles"? Es canvia (o s'afegeix) un iterador; els clients no es toquen.

Segon cas: historial de comandes paginat

"Les meves comandes" mostra l'historial complet del client, però el servidor el lliura per pàgines de 20. El client del codi no hauria de saber res de pàgines — només vol "la comanda següent". L'iterador és el lloc perfecte per amagar la paginació:

public class IteradorHistorialComandes implements Iterator<Comanda> {

    private final ServeiHistorial servei;
    private final String idClient;
    private List<Comanda> paginaActual = List.of();
    private int posicioEnPagina = 0;
    private int numeroPagina = 0;
    private boolean ultimaPagina = false;

    public IteradorHistorialComandes(ServeiHistorial servei, String idClient) {
        this.servei = servei;
        this.idClient = idClient;
    }

    @Override
    public boolean hasNext() {
        if (posicioEnPagina < paginaActual.size()) {
            return true;
        }
        if (ultimaPagina) {
            return false;
        }
        paginaActual = servei.pagina(idClient, numeroPagina++, 20); // carrega mandrosa
        posicioEnPagina = 0;
        ultimaPagina = paginaActual.size() < 20;
        return !paginaActual.isEmpty();
    }

    @Override
    public Comanda next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        return paginaActual.get(posicioEnPagina++);
    }
}

L'elegància és en el que el client no veu: peticions de xarxa, mides de pàgina, detecció de l'última. I en el que no passa: si la UI només mostra els 20 primers, la pàgina 2 no es demana mai — l'iterador és mandrós per construcció, parent del proxy virtual en esperit: no facis la feina fins que la demanin.

Iteradors externs vs. interns, i Streams com a evolució

Tot l'anterior és iteració externa: el client controla el bucle — demana element a element amb next(), i pot parar, intercalar o portar dos iteradors alhora. L'alternativa és la iteració interna: lliures a la col·lecció l'operació, i ella controla el bucle:

carta.forEach(plat -> index.afegir(plat));       // interna: el bucle es a dins
Externa (Iterator) Interna (forEach, Streams)
Qui controla el bucle El client La col·lecció/l'stream
Parar a mitges, intercalar dos recorreguts Fàcil Difícil o impossible
Concisió, encadenar operacions Verbós Excel·lent
Paral·lelitzar Manual parallelStream()

Els Streams de Java (Java 8+) són la iteració interna portada a la maduresa, i descendeixen directament d'aquest patró: un Stream es construeix sobre un Spliterator (un iterador amb suport de partició per a paral·lelisme), és mandrós com el nostre iterador paginat, i amaga el recorregut rere operacions declaratives:

List<String> senseGluten = StreamSupport.stream(carta.spliterator(), false)
        .filter(Plat::esSenseGluten)
        .map(Plat::getNom)
        .sorted()
        .toList();

Fixa't en el dividend: com que vam fer SeccioCarta implements Iterable, els Streams ens surten gratis — carta.spliterator() ve de sèrie amb Iterable. La regla pràctica moderna: exposa Iterable/Stream a les teves col·leccions de domini; escriu un Iterator a mà només quan el recorregut en si té lògica per encapsular (aplanar un arbre, paginar una API, barrejar fonts).

Quan usar-lo i quan no

Usa'l quan:

  • Els clients recorren una estructura no trivial (arbre, pàgines remotes, diverses fonts) i no n'han de conèixer la representació.
  • Vols diversos recorreguts (ordre natural, filtrat, en amplada) o diversos de simultanis sobre la mateixa col·lecció.
  • Vols recorreguts mandrosos sobre dades cares d'obtenir.

Evita'l quan:

  • La col·lecció és una List normal i el recorregut és de principi a fi: el for-each i els Streams del JDK ja són el patró; escriure el teu propi Iterator seria reimplementar l'estàndard.
  • El client necessita accés posicional o aleatori (get(i)): el contracte seqüencial de l'iterador fa nosa.

Cost real: els iteradors fets a mà tenen la seva pròpia lògica delicada (invariants, estat entre crides) i són un focus típic d'errors off-by-one — escriu-los tests com a qualsevol classe amb estat.

Relació amb altres patrons

  • Composite: parella natural — el composite defineix l'arbre, l'iterador el recorre sense exposar-lo. Amb Visitor formaran el trio complet d'"estructura + recorregut + operacions".
  • Factory Method: iterator() és un factory method de manual.
  • Memento: GoF apunta que un iterador pot capturar la seva posició en un memento per reprendre-la.
  • Proxy: l'iterador paginat comparteix esperit amb el proxy virtual (mandra davant recursos cars).

Errors comuns

  • Modificar la col·lecció mentre s'itera: els iteradors del JDK són fail-fast i llancen ConcurrentModificationException. Per esborrar durant el recorregut usa iterator.remove(), removeIf(...), o itera sobre una còpia.
  • hasNext() amb efectes que es repeteixen: els clients poden cridar hasNext() diverses vegades seguides; ha de ser idempotent (el nostre paginat carrega pàgina només quan s'esgota l'actual, el cridin les vegades que el cridin).
  • Guardar l'estat del recorregut a la col·lecció ("un cursor intern"): mata els recorreguts simultanis i deixa la col·lecció en estat inconsistent si un recorregut s'abandona a mitges.
  • Retornar null com a fi de recorregut en comptes de hasNext() == false + NoSuchElementException: trenca el contracte que el for-each i tot l'ecosistema esperen.
  • Iteradors que exposen el que venien a ocultar: si el teu Iterator<ComponentCarta> obliga el client a fer instanceof amb el que rep, el problema segueix allà; tria bé el tipus d'element (nosaltres retornem Plat, no ComponentCarta).

Exercicis

Exercici 1: iterador filtrat

Implementa IteradorDisponibles, que embolcalla un Iterator<Plat> qualsevol i només lliura plats amb estaDisponible() == true. (Pista: és un iterador-decorador; necessitaràs "mirar per endavant" el proper vàlid per poder respondre hasNext().)

Exercici 2: for-each sobre l'historial

Fes que l'historial es pugui usar amb for-each: escriu HistorialComandes implements Iterable<Comanda> recolzant-te en IteradorHistorialComandes. Per què és important que iterator() retorni un iterador nou a cada crida?

Exercici 3: iterador a mà o Stream?

Per a cada cas, digues si escriuries un Iterator propi o usaries Streams/for-each sobre l'existent: (a) sumar els preus d'una List<LiniaComanda>; (b) recórrer les comandes d'avui barrejant dues fonts ordenades (app i telèfon) en ordre cronològic global; (c) els 5 plats més cars de la carta.

Solucions

Solució 1:

public class IteradorDisponibles implements Iterator<Plat> {

    private final Iterator<Plat> origen;
    private Plat proper;                   // el seguent valid, ja localitzat

    public IteradorDisponibles(Iterator<Plat> origen) {
        this.origen = origen;
        avancar();
    }

    private void avancar() {
        proper = null;
        while (origen.hasNext()) {
            Plat candidat = origen.next();
            if (candidat.estaDisponible()) {
                proper = candidat;
                return;
            }
        }
    }

    @Override public boolean hasNext() { return proper != null; }

    @Override public Plat next() {
        if (proper == null) throw new NoSuchElementException();
        Plat resultat = proper;
        avancar();
        return resultat;
    }
}

El "mirar per endavant" és obligatori: sense localitzar el proper vàlid, hasNext() no podria respondre amb veritat. I sí, és la mecànica de Decorator aplicada a iteradors — mateixa interfície, embolcalla, filtra.

Solució 2:

public class HistorialComandes implements Iterable<Comanda> {

    private final ServeiHistorial servei;
    private final String idClient;

    public HistorialComandes(ServeiHistorial servei, String idClient) {
        this.servei = servei;
        this.idClient = idClient;
    }

    @Override
    public Iterator<Comanda> iterator() {
        return new IteradorHistorialComandes(servei, idClient);   // nou cada vegada!
    }
}

Ha de ser nou a cada crida perquè l'estat del recorregut viu a l'iterador: si iterator() retornés sempre el mateix, el segon for-each començaria on va acabar el primer (o dos recorreguts simultanis es trepitjarien les pàgines).

Solució 3: (a) Stream d'una línia: linies.stream().map(LiniaComanda::getPreu).reduce(ZERO, BigDecimal::add) — res a encapsular; (b) iterador a mà: la barreja ordenada de dues fonts és lògica de recorregut genuïna (comparar caps, avançar la menor) que mereix la seva classe i els seus tests; (c) Stream sobre l'Iterable que ja tenim: stream(carta.spliterator(), false).sorted(comparing(Plat::getPreu).reversed()).limit(5).

Conclusió

Iterator reïfica el recorregut: la posició i la lògica d'avanç viuen en un objecte amb dos mètodes, l'estructura interna queda segellada, i diversos recorreguts —complets, filtrats, mandrosos, simultanis— conviuen sense tocar-se. A PideYa, la carta Composite s'aplana rere Iterable<Plat> (adeu a les recursions duplicades amb instanceof) i l'historial amaga la seva paginació de xarxa rere un hasNext(). I has vist la genealogia completa: del GoF a l'Iterator/Iterable del JDK, del for-each com a sucre sintàctic als Streams com a iteració interna, mandrosa i paral·lelitzable.

Fins aquí, patrons que organitzen el diàleg entre dues parts: client i col·lecció, invocador i ordre. El següent ataca les converses multitudinàries. Al repartiment de PideYa, les comandes llestes, els repartidors disponibles i la cuina es criden els uns als altres en una malla on tothom coneix tothom — i cada peça nova multiplica les connexions. La solució: que ningú no parli amb ningú... llevat que sigui a través d'un. Ens veiem a Mediator.

Curs de Patrons de Disseny de Programari

Mòdul 1: Introducció als Patrons de Disseny

Mòdul 2: Patrons Creacionals

Mòdul 3: Patrons Estructurals

Mòdul 4: Patrons de Comportament

Mòdul 5: Aplicació de Patrons de Disseny

Mòdul 6: Patrons de Disseny Avançats

Mòdul 7: Recursos Addicionals i Conclusió

© Copyright 2026. Tots els drets reservats