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
- El problema a PideYa: operacions que no caben a la carta
- L'obstacle tècnic: Java tria mètodes amb un sol tipus
- Double dispatch, pas a pas
- Intenció i estructura del patró
- Implementació Java completa
- Els dos eixos: què abarateix Visitor i què encareix
- L'alternativa moderna: pattern matching de Java 21
- Quan usar-lo i quan no
- Relació amb altres patrons
- Errors comuns
- 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:
Platacumula 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
instanceofdes de fora— dispersa pel codi els mateixosif (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çó:
- El client crida
node.acceptar(visitant)ambnodede tipus estàticComponentCarta. - Primer despatx (dinàmic, pel node): la JVM tria l'
acceptarde la classe real — el dePlat, posem. Dins d'aquell mètode, ja no hi ha dubte:thisés unPlat, amb tipus estàticPlat. - Aquell
acceptarfa la segona crida:visitant.visitar(this). Com que el tipus estàtic dethisésPlat, el compilador lliga la sobrecàrregavisitar(Plat). Segon despatx (dinàmic, pel visitant): la JVM tria la implementació de la classe real del visitant —ExportadorJson.visitar(Plat). - 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'
instanceofsense 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'
acceptares 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
acceptara la classe base (visitant.visitar(this)ambthisde tipus estàticComponentCarta) no compila o lliga malament la sobrecàrrega; cada classe concreta ha de tenir el seu propiacceptar, encara que semblin idèntics — no són duplicació, són el mecanisme. - Visitants reutilitzats sense reiniciar:
CalculadoraAlergensacumula; passar-la per dues cartes seguides barreja resultats. Un visitant amb estat és d'usar-una-vegada (o dona-li unreiniciar()explícit). - Recorregut duplicat o absent: si
SeccioCarta.acceptarrecorre fills i el visitant també els recorre avisitar(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
- Què són els Patrons de Disseny?
- Història i Origen dels Patrons de Disseny
- Principis de Disseny: SOLID i Altres Fonaments
- UML Essencial per Entendre Patrons
- Classificació dels Patrons de Disseny
- Avantatges i Desavantatges d'Usar Patrons de Disseny
Mòdul 2: Patrons Creacionals
- Introducció als Patrons Creacionals
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa i Elecció de Patrons Creacionals
Mòdul 3: Patrons Estructurals
- Introducció als Patrons Estructurals
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa i Elecció de Patrons Estructurals
Mòdul 4: Patrons de Comportament
- Introducció als Patrons de Comportament
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa i Elecció de Patrons de Comportament
Mòdul 5: Aplicació de Patrons de Disseny
- Com Seleccionar el Patró Adequat
- Exemples Pràctics d'Ús de Patrons
- Patrons de Disseny en Projectes Reals
- Refactorització Usant Patrons de Disseny
- Antipatrons: Quan els Patrons es Tornen un Problema
Mòdul 6: Patrons de Disseny Avançats
- Patrons de Disseny en Arquitectures Modernes
- Patrons de Disseny en Microserveis
- Patrons de Disseny en Sistemes Distribuïts
- Patrons de Concurrència
- Patrons de Disseny en Desenvolupament Àgil
