La carta Composite de PideYa porta dos mòduls acumulant pretendents: exportar-la a JSON per a l'app, calcular els al·lèrgens acumulats de cada secció, auditar preus contra la política de la cadena... Cada operació nova amenaça d'engreixar Plat i SeccioCarta amb mètodes que no són assumpte seu — què hi pinta exportarAJson() en una classe de domini? Visitor inverteix el repartiment: la jerarquia queda congelada amb un únic mètode d'entrada (acceptar), i cada operació nova és una classe visitant a part, afegible sense tocar un sol node. El preu del truc és el més alt del catàleg en complexitat conceptual —un mecanisme anomenat double dispatch que veurem pas a pas— i un compromís molt concret: la jerarquia ha de ser estable, perquè cada node nou trenca tots els visitants.

Contingut

  1. El problema a PideYa: operacions que no caben a la carta
  2. L'obstacle tècnic: Java tria mètodes amb un sol tipus
  3. Double dispatch, pas a pas
  4. Intenció i estructura del patró
  5. Implementació Java completa
  6. Els dos eixos: què abarateix Visitor i què encareix
  7. L'alternativa moderna: pattern matching de Java 21
  8. Quan usar-lo i quan no
  9. Relació amb altres patrons
  10. Errors comuns
  11. Exercicis i conclusió

El problema a PideYa: operacions que no caben a la carta

Recordem la jerarquia del mòdul 3: ComponentCarta amb filles Plat (fulla), SeccioCarta (grup recursiu) i Menu (composició de plats amb preu propi). Les operacions de domini (preu, disponibilitat) viuen a dins, i allà estan bé. Però les peticions que arriben ara són d'una altra espècie:

  • Exportar a JSON per a l'app mòbil (assumpte d'integració, no de domini).
  • Calcular al·lèrgens agregats per secció (assumpte de l'equip de salut alimentària).
  • Auditar preus (algun plat per sota del cost?, menús més cars que la suma de les seves parts?) — assumpte de negoci-finances.

Primer impuls: un mètode per operació a ComponentCarta, implementat a cada node. Tres operacions × tres classes = nou mètodes... i els problemes:

  • La jerarquia es converteix en calaix de sastre: Plat acumula responsabilitats d'integració, salut i finances (SRP polvoritzat); cada equip toca les mateixes classes centrals, amb conflictes i desplegaments creuats.
  • Cada operació nova modifica tota la jerarquia (adeu OCP a l'eix d'operacions).
  • L'alternativa artesanal —recórrer amb instanceof des de fora— dispersa pel codi els mateixos if (x instanceof Plat)... else if (x instanceof SeccioCarta)... que l'Iterator va venir a erradicar, i el compilador no avisa quan falta un cas.

El que volem, dit amb precisió: les operacions fora de la jerarquia (una classe per operació), però amb despatx segur per tipus de node (que el compilador obligui a cobrir plats, seccions i menús). Allà hi ha un obstacle tècnic seriós.

L'obstacle tècnic: Java tria mètodes amb un sol tipus

Intentem l'operació externa amb sobrecàrrega:

public class ExportadorJson {
    public String exportar(Plat plat) { /* ... */ }
    public String exportar(SeccioCarta seccio) { /* ... */ }
    public String exportar(Menu menu) { /* ... */ }
}

ComponentCarta node = carta.getFills().get(0);    // tipus estatic: ComponentCarta
exportador.exportar(node);                        // ERROR de compilacio!

No compila (no hi ha exportar(ComponentCarta)), i és el símptoma d'una regla profunda: la sobrecàrrega es resol en compilació amb el tipus estàtic de l'argument. Java només despatxa dinàmicament per un tipus: el del receptor de la crida (objecte.metode() tria la implementació segons la classe real d'objecte). Això és single dispatch. Triar mètode segons dos tipus reals alhora —quina operació? i quin node?— és double dispatch, i Java no el porta de sèrie. Visitor és, en essència, el truc per simular-lo amb dues crides de single dispatch.

Double dispatch, pas a pas

El truc complet en quatre passos. Llegeix-lo dues vegades; és el cor de la lliçó:

  1. El client crida node.acceptar(visitant) amb node de tipus estàtic ComponentCarta.
  2. Primer despatx (dinàmic, pel node): la JVM tria l'acceptar de la classe real — el de Plat, posem. Dins d'aquell mètode, ja no hi ha dubte: this és un Plat, amb tipus estàtic Plat.
  3. Aquell acceptar fa la segona crida: visitant.visitar(this). Com que el tipus estàtic de this és Plat, el compilador lliga la sobrecàrrega visitar(Plat). Segon despatx (dinàmic, pel visitant): la JVM tria la implementació de la classe real del visitant — ExportadorJson.visitar(Plat).
  4. Resultat: s'ha executat el mètode que correspon a la combinació (node real, visitant real). Dues crides virtuals encadenades = double dispatch.
sequenceDiagram
    participant C as Client
    participant P as Plat (node real)
    participant V as ExportadorJson (visitant real)
    C->>P: acceptar(visitant)
    Note over P: 1r despatx: la JVM ha triat<br/>l'acceptar() de Plat,<br/>aquí this ÉS Plat
    P->>V: visitar(this)
    Note over V: 2n despatx: s'executa<br/>ExportadorJson.visitar(Plat)
    V-->>C: (operació aplicada al tipus exacte)

La frase per recordar-ho: el node no fa l'operació; el node confessa el seu tipus cridant la sobrecàrrega correcta del visitant. L'acceptar de cada node és idèntic en aparença (visitant.visitar(this)) però cadascun lliga una sobrecàrrega diferent, perquè el tipus estàtic de this és diferent a cada classe.

Intenció i estructura del patró

Intenció (GoF): representar una operació a fer sobre els elements d'una estructura d'objectes. Visitor permet definir una operació nova sense canviar les classes dels elements sobre els quals opera.

classDiagram
    class VisitantCarta {
        <<interface>>
        +visitar(plat: Plat)
        +visitar(seccio: SeccioCarta)
        +visitar(menu: Menu)
    }
    class ComponentCarta {
        <<abstract>>
        +acceptar(v: VisitantCarta)*
    }
    class Plat {
        +acceptar(v: VisitantCarta)
    }
    class SeccioCarta {
        +acceptar(v: VisitantCarta)
    }
    class Menu {
        +acceptar(v: VisitantCarta)
    }
    class CalculadoraAlergens {
        -acumulats: Set~Alergen~
        +visitar(plat) +visitar(seccio) +visitar(menu)
    }
    class ExportadorJson {
        +visitar(plat) +visitar(seccio) +visitar(menu)
    }
    class AuditorPreus {
        -incidencies: List~String~
        +visitar(plat) +visitar(seccio) +visitar(menu)
    }

    ComponentCarta <|-- Plat
    ComponentCarta <|-- SeccioCarta
    ComponentCarta <|-- Menu
    VisitantCarta <|.. ExportadorJson
    VisitantCarta <|.. CalculadoraAlergens
    VisitantCarta <|.. AuditorPreus
    ComponentCarta ..> VisitantCarta : acceptar(v) crida v.visitar(this)
Rol GoF A PideYa
Visitor (una sobrecàrrega visitar per tipus de node) VisitantCarta
ConcreteVisitor (una classe per operació) ExportadorJson, CalculadoraAlergens, AuditorPreus
Element (declara acceptar) ComponentCarta
ConcreteElement Plat, SeccioCarta, Menu
ObjectStructure (on viuen els nodes) la carta Composite del mòdul 3

Implementació Java completa

L'únic canvi a la jerarquia — es fa una vegada i es congela. És la concessió honesta del patró: per no tocar la jerarquia mai més, cal tocar-la una vegada:

public interface VisitantCarta {
    void visitar(Plat plat);
    void visitar(SeccioCarta seccio);
    void visitar(Menu menu);
}

public abstract class ComponentCarta {
    // ... tot el del modul 3 intacte ...
    public abstract void acceptar(VisitantCarta visitant);
}

public class Plat extends ComponentCarta {
    @Override
    public void acceptar(VisitantCarta visitant) {
        visitant.visitar(this);               // this es Plat → lliga visitar(Plat)
    }
}

public class SeccioCarta extends ComponentCarta {
    @Override
    public void acceptar(VisitantCarta visitant) {
        visitant.visitar(this);               // lliga visitar(SeccioCarta)
        for (ComponentCarta fill : getFills()) {
            fill.acceptar(visitant);          // recorregut del composite: aqui
        }
    }
}

public class Menu extends ComponentCarta {
    @Override
    public void acceptar(VisitantCarta visitant) {
        visitant.visitar(this);               // lliga visitar(Menu)
        // decisio de disseny: el menu NO recorre els seus plats interns
        // (cada visitant decideix a visitar(Menu) si li interessen)
    }
}

Fixa't en la decisió incrustada: SeccioCarta.acceptar recorre els seus fills — el recorregut viu a l'estructura, i els visitants reben els nodes un a un sense saber navegar. (Alternativa igual de vàlida: recorregut al visitant, o delegat a un Iterator; l'important és decidir-ho una vegada i documentar-ho.)

Primera operació: els al·lèrgens. Un visitant amb estat: acumula durant el recorregut i es cull al final — el modisme típic:

public class CalculadoraAlergens implements VisitantCarta {

    private final Set<Alergen> acumulats = EnumSet.noneOf(Alergen.class);

    @Override
    public void visitar(Plat plat) {
        acumulats.addAll(plat.getAlergens());
    }

    @Override
    public void visitar(SeccioCarta seccio) {
        // res: els allergens son a les fulles; els fills arribaran sols
    }

    @Override
    public void visitar(Menu menu) {
        menu.getPlats().forEach(p -> acumulats.addAll(p.getAlergens()));
    }

    public Set<Alergen> getResultat() {
        return Set.copyOf(acumulats);
    }
}

Segona operació: l'auditoria de preus — la que ensenya per què les sobrecàrregues per tipus valen or: cada tipus de node té la seva pròpia regla:

public class AuditorPreus implements VisitantCarta {

    private final List<String> incidencies = new ArrayList<>();

    @Override
    public void visitar(Plat plat) {
        if (plat.getPreu().compareTo(plat.getCost()) <= 0) {
            incidencies.add("Plat sota cost: " + plat.getNom());
        }
    }

    @Override
    public void visitar(SeccioCarta seccio) {
        if (seccio.getFills().isEmpty()) {
            incidencies.add("Seccio buida publicada: " + seccio.getNom());
        }
    }

    @Override
    public void visitar(Menu menu) {
        BigDecimal sumaParts = menu.getPlats().stream()
                .map(Plat::getPreu).reduce(BigDecimal.ZERO, BigDecimal::add);
        if (menu.getPreu().compareTo(sumaParts) > 0) {
            incidencies.add("Menu mes car que les seves parts: " + menu.getNom());
        }
    }

    public List<String> getIncidencies() { return List.copyOf(incidencies); }
}

Ús — i aquí es veu el patró sencer en tres línies:

AuditorPreus auditor = new AuditorPreus();
carta.acceptar(auditor);                         // un recorregut, despatxos exactes
auditor.getIncidencies().forEach(RegistreEsdeveniments.INSTANCIA::avis);

L'operació nova de demà ("comptar plats vegans per secció", "generar el PDF de la carta") serà una classe nova que implementa VisitantCarta — ni Plat, ni SeccioCarta, ni Menu no es tornaran a tocar. I si la interfície del visitant guanya un mètode, el compilador obliga cada visitant a pronunciar-se: cobertura per tipus garantida, sense instanceof ni casos oblidats.

Els dos eixos: què abarateix Visitor i què encareix

El patró és un intercanvi d'eixos d'extensibilitat, i triar-lo bé exigeix veure la taula completa:

Canvi Sense Visitor (mètodes a la jerarquia) Amb Visitor
Operació nova Tocar totes les classes de node Una classe visitant nova; nodes intactes
Tipus de node nou Una classe nova; operacions existents intactes (cadascuna implementada en ella) Tocar la interfície visitant i TOTS els visitants

Aquesta segona fila és el preu famós del patró: si demà la carta guanya un node Combo, cal afegir visitar(Combo) a VisitantCarta i a cada visitant existent (el compilador almenys els assenyalarà tots). D'aquí la regla de decisió canònica: Visitor quan la jerarquia és estable i les operacions proliferen; mètodes a la jerarquia quan els tipus proliferen i les operacions són estables. La carta de PideYa compleix el perfil: Plat/SeccioCarta/Menu no canvien des del mòdul 3, mentre els tres equips fan cua amb operacions noves.

Dos costos menors més però reals: el double dispatch desconcerta qui no el coneix (el flux rebota entre classes; documenta el patró pel seu nom), i els visitants només veuen l'API pública dels nodes — si una operació necessita tripes privades, o obres accessors (afeblint l'encapsulació que Memento tant va protegir) o aquella operació pertany a dins.

L'alternativa moderna: pattern matching de Java 21

La menció promesa: des de Java 21, el switch amb patrons i les jerarquies sealed ataquen el mateix problema sense cerimonial:

public sealed interface ComponentCarta permits Plat, SeccioCarta, Menu { }

public String exportarJson(ComponentCarta node) {
    return switch (node) {
        case Plat p         -> "{\"plat\":\"" + p.getNom() + "\",\"preu\":" + p.getPreu() + "}";
        case SeccioCarta s  -> "{\"seccio\":\"" + s.getNom() + "\",\"fills\":["
                                 + s.getFills().stream().map(this::exportarJson)
                                     .collect(joining(",")) + "]}";
        case Menu m         -> "{\"menu\":\"" + m.getNom() + "\",\"preu\":" + m.getPreu() + "}";
        // sense default: en ser sealed, el compilador EXIGEIX cobrir tots els tipus
    };
}

El crucial és el sealed: la jerarquia declara les seves filles permeses i el switch sense default obliga a l'exhaustivitat — la mateixa garantia "operació nova sense tocar nodes + cobertura per tipus verificada pel compilador" que Visitor aconseguia amb el double dispatch, ara amb una funció i zero mètodes acceptar. Si a més el node nou apareix, tots els switches sense default deixen de compilar: el mateix avís que donaven els visitants. En codi Java 21+ amb jerarquies segellades, aquesta és avui l'opció per defecte per a operacions externes; Visitor conserva l'avantatge quan l'operació és gran i amb estat (una classe l'organitza millor que un switch quilomètric), quan la jerarquia no es pot segellar (plugins, nodes de tercers), o en bases de codi anteriors a Java 21.

Quan usar-lo i quan no

Usa'l quan:

  • Una estructura d'objectes estable necessita moltes operacions no relacionades entre si, i ficar-les a dins contaminaria les classes (o les firmarien equips diferents).
  • Necessites despatx per tipus garantit pel compilador en operar des de fora (res d'instanceof sense xarxa).
  • Les operacions necessiten acumular estat durant el recorregut (exportadors, auditors, calculadores).

Evita'l quan:

  • La jerarquia canvia sovint: cada node nou trenca tots els visitants — estàs comprant extensibilitat a l'eix equivocat.
  • Hi ha una o dues operacions estables: el cerimonial (interfície + acceptar a cada node) no s'amortitza; mètodes normals o un switch segellat basten.
  • Treballes en Java 21+ amb jerarquia segellada i les operacions són funcions simples: el pattern matching dona el mateix amb molt menys.

Relació amb altres patrons

  • Composite: la seva parella històrica — el composite defineix l'estructura, el visitant hi afegeix operacions. Amb Iterator formen el trio estructura/recorregut/operacions; de fet el recorregut d'acceptar es pot delegar en l'iterador del mòdul anterior.
  • Interpreter: els AST són l'hàbitat clàssic de Visitor (avaluar, imprimir, optimitzar l'arbre com a visitants diferents) — ho vam mencionar allà.
  • Iterator: Iterator lliura elements uniformes (el nostre Iterator<Plat> aplanava); Visitor distingeix cada tipus de node. Es trien per aquesta pregunta: recórrer o despatxar?
  • Command: un visitant amb estat que registra accions per node pot generar ordres a executar després.

Errors comuns

  • Trencar el double dispatch amb dreceres: un acceptar a la classe base (visitant.visitar(this) amb this de tipus estàtic ComponentCarta) no compila o lliga malament la sobrecàrrega; cada classe concreta ha de tenir el seu propi acceptar, encara que semblin idèntics — no són duplicació, són el mecanisme.
  • Visitants reutilitzats sense reiniciar: CalculadoraAlergens acumula; passar-la per dues cartes seguides barreja resultats. Un visitant amb estat és d'usar-una-vegada (o dona-li un reiniciar() explícit).
  • Recorregut duplicat o absent: si SeccioCarta.acceptar recorre fills i el visitant també els recorre a visitar(SeccioCarta), cada node es visita dues vegades; si no ho fa ningú, cap. Decideix l'amo del recorregut (estructura o visitant) i sigues conseqüent a tota la jerarquia.
  • Obrir les tripes dels nodes per servir un visitant (getters d'estat intern "només per a l'exportació"): si l'operació necessita allò privat, el seu lloc és dins de la classe, no en un visitant.
  • Aplicar Visitor a jerarquies volàtils "perquè és el patró elegant": rellegeix la taula dels dos eixos; amb tipus canviants, és l'elecció exactament oposada a la correcta.

Exercicis

Exercici 1: el comptador de la carta

Implementa EstadistiquesCarta implements VisitantCarta, que en un sol recorregut compti plats, seccions i menús, i calculi el preu mitjà dels plats. Escriu també les tres línies d'ús.

Exercici 2: seguir el double dispatch amb el dit

Donat ComponentCarta node = new Menu("Menu del dia", ...) i VisitantCarta v = new AuditorPreus(), la crida és node.acceptar(v). Enumera, en ordre: (1) quin mètode tria la JVM al primer despatx i amb quin criteri; (2) quina sobrecàrrega lliga el compilador dins d'aquell mètode i per què; (3) quina implementació executa la JVM al segon despatx. Assenyala quina decisió és de compilació i quina d'execució.

Exercici 3: Visitor o pattern matching?

Per a cada escenari, tria Visitor clàssic o switch amb patrons sobre jerarquia segellada, i justifica-ho: (a) projecte en Java 17 (sense pattern matching complet sobre switch), carta estable, cinc operacions previstes; (b) Java 21, jerarquia segellada, una funció "profunditat màxima de l'arbre"; (c) Java 21, però els nodes de la carta poden ser aportats per mòduls de tercers (jerarquia oberta), i l'exportació acumula estat complex.

Solucions

Solució 1:

public class EstadistiquesCarta implements VisitantCarta {

    private int plats, seccions, menus;
    private BigDecimal sumaPreus = BigDecimal.ZERO;

    @Override public void visitar(Plat plat) {
        plats++;
        sumaPreus = sumaPreus.add(plat.getPreu());
    }

    @Override public void visitar(SeccioCarta seccio) { seccions++; }

    @Override public void visitar(Menu menu) { menus++; }

    public String resum() {
        BigDecimal mitja = plats == 0 ? BigDecimal.ZERO
                : sumaPreus.divide(BigDecimal.valueOf(plats), 2, RoundingMode.HALF_UP);
        return plats + " plats, " + seccions + " seccions, " + menus
                + " menus; preu mitja " + mitja + " €";
    }
}

EstadistiquesCarta stats = new EstadistiquesCarta();
carta.acceptar(stats);
System.out.println(stats.resum());

Solució 2: (1) primer despatx — la JVM tria Menu.acceptar(VisitantCarta) segons la classe real de node (Menu), decisió d'execució; (2) dins d'aquell mètode, el compilador lliga visitar(Menu) perquè el tipus estàtic de this a Menu.acceptar és Menu — decisió de compilació (elecció de sobrecàrrega); (3) segon despatx — la JVM executa AuditorPreus.visitar(Menu) segons la classe real del visitant, decisió d'execució. Aquest sandvitx —dinàmica, estàtica, dinàmica— és el double dispatch complet.

Solució 3: (a) Visitor: sense pattern matching exhaustiu disponible, és l'únic que dona cobertura per tipus verificada pel compilador, i amb cinc operacions el cerimonial s'amortitza; (b) switch amb patrons: funció petita, jerarquia segellada amb exhaustivitat garantida — un visitant per a això és litúrgia buida; (c) Visitor: la jerarquia no es pot segellar (el switch perd l'exhaustivitat, el seu gran argument) i l'operació amb estat complex encaixa millor en una classe visitant; a més els mòduls de tercers poden portar el seu acceptar implementat, cosa que un switch central no pot preveure.

Conclusió

Visitor tanca el catàleg de comportament amb el seu intercanvi característic: la carta va quedar congelada rere un únic acceptar per node, i les operacions —exportar, al·lèrgens, auditoria, estadístiques— viuen cadascuna a la seva classe, afegibles sense fregar Plat, SeccioCarta ni Menu, amb el compilador garantint la cobertura per tipus. Has entès el motor per dins —dos despatxos de single dispatch encadenats que simulen el double dispatch que Java no té— i la seva factura exacta: jerarquia estable o visitants trencats, més l'alternativa moderna del pattern matching sobre jerarquies segellades que en Java 21+ cobreix els casos simples amb molt menys soroll.

I amb això, els onze són sobre la taula. Onze patrons de comportament en onze lliçons: peticions que viatgen per cadenes, es congelen en ordres i s'interpreten com a llenguatges; recorreguts, mediacions i fotografies; observadors, estats, estratègies, plantilles i visitants. Massa per triar de memòria: falta la lliçó que els posa cara a cara —les parelles que tothom confon, el flowchart de decisió, les combinacions que funcionen juntes— i que repassa què va quedar instal·lat a cada racó de PideYa. Ens veiem a la Comparativa i Elecció de Patrons de Comportament.

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