Bridge connectava dues jerarquies; avui l'estructura creix cap endins. La carta d'un restaurant a PideYa no és una llista plana: és un arbre. Seccions ("Pizzes", "Begudes") que contenen subseccions ("Begudes > Refrescos", "Begudes > Vins") que contenen plats; i menús que agrupen plats —i de vegades altres menús— amb preu conjunt. Cada vegada que el codi pregunta "això és un plat o una secció?" abans d'operar, s'escriu un if que es repetirà en vint llocs. Composite elimina aquesta pregunta: fulles i grups comparteixen interfície, i l'arbre sencer es tracta igual que un sol objecte.
Contingut
- El problema a PideYa: la carta és un arbre
- Intenció i estructura del patró
- Implementació Java completa
- Recorreguts recursius
- Menús: quan el grup també es ven
- Transparència davant de seguretat
- Quan usar-lo i quan no
- Errors comuns, exercicis i conclusió
El problema a PideYa: la carta és un arbre
La carta de La Bella Napoli, un dels restaurants estrella de PideYa, té aquesta pinta:
Carta La Bella Napoli
├── Pizzes
│ ├── Classiques
│ │ ├── Margarita ........ 8,50 €
│ │ └── Prosciutto ....... 9,90 €
│ └── Especials
│ └── Tartufo .......... 13,50 €
├── Pastes
│ └── Carbonara ............ 10,50 €
└── Begudes
├── Aigua ................ 1,50 €
└── Refrescos
└── Cola ............. 2,20 €Profunditat arbitrària: seccions dins de seccions, sense límit pactat. El primer model de l'equip va usar dues classes sense relació i col·leccions separades:
// Primer intent: dos mons separats. NO imitar.
public class Seccio {
private String nom;
private List<Seccio> subseccions;
private List<Plat> plats;
}I tot el codi que toca la carta va acabar infestat del mateix doble bucle amb pregunta:
// Per mostrar la carta... i una altra copia per a disponibilitat... i una altra per buscar...
void mostrar(Seccio seccio, int nivell) {
imprimir(seccio.getNom(), nivell);
for (Plat p : seccio.getPlats()) { // branca 1: fulles
imprimir(p.getNom() + " " + p.getPreu(), nivell + 1);
}
for (Seccio s : seccio.getSubseccions()) { // branca 2: grups
mostrar(s, nivell + 1);
}
}Cada operació nova (està disponible aquesta secció? quants plats sense gluten hi ha? què mostrar si la cuina tanca la graella?) duplica l'estructura de doble branca. I quan negoci demani que un menú contingui altres menús, les dues llistes paral·leles ja no donaran més de si. El símptoma, en net: una estructura part-tot recursiva tractada amb condicionals en lloc de amb una interfície comuna.
Intenció i estructura del patró
Intenció (GoF): compondre objectes en estructures d'arbre per representar jerarquies part-tot. Composite permet als clients tractar de manera uniforme tant els objectes individuals com les composicions d'objectes.
Les dues idees del patró, que convé separar:
- Una interfície comuna (Component) per a fulles i grups: les operacions que tenen sentit en tots dos (
mostrar,estaDisponible...). - El grup conté Components, no fulles ni grups concrets: per això la niada és il·limitada gratis — una secció que conté seccions que contenen plats és simplement un Composite amb fills Component.
classDiagram
class ComponentCarta {
<<interface>>
+getNom() String
+estaDisponible() boolean
+comptarPlatsDisponibles() int
+mostrar(nivell: int)
}
class Plat {
-nom: String
-preu: BigDecimal
-disponible: boolean
+estaDisponible() boolean
+comptarPlatsDisponibles() int
+mostrar(nivell: int)
}
class SeccioCarta {
-nom: String
-fills: List~ComponentCarta~
+afegir(fill: ComponentCarta)
+treure(fill: ComponentCarta)
+estaDisponible() boolean
+comptarPlatsDisponibles() int
+mostrar(nivell: int)
}
class AppClient
ComponentCarta <|.. Plat
ComponentCarta <|.. SeccioCarta
SeccioCarta *-- ComponentCarta : fills
AppClient --> ComponentCarta : usa
| Rol GoF | A PideYa |
|---|---|
| Component (interfície comuna) | ComponentCarta |
| Leaf (fulla, sense fills) | Plat |
| Composite (grup, amb fills Component) | SeccioCarta (i després Menu) |
| Client | L'app, el cercador, el motor de disponibilitat |
La fletxa clau del diagrama és la composició SeccioCarta *-- ComponentCarta — la mateixa relació de rombe ple que vam aprendre amb Comanda *-- LiniaComanda a la lliçó d'UML, i aquí a més recursiva: el composite conté elements de la seva pròpia interfície. Aquesta fletxa que torna és la firma visual del patró.
Implementació Java completa
El Component — només operacions amb sentit en fulles i en grups:
public interface ComponentCarta {
String getNom();
boolean estaDisponible();
int comptarPlatsDisponibles();
void mostrar(int nivell);
}La fulla: un plat respon per si mateix, sense recursió:
public class Plat implements ComponentCarta {
private final String nom;
private final BigDecimal preu;
private boolean disponible = true; // la cuina pot exhaurir-lo
public Plat(String nom, BigDecimal preu) {
this.nom = nom;
this.preu = preu;
}
@Override public String getNom() { return nom; }
public BigDecimal getPreu() { return preu; }
public void exhaurir() { this.disponible = false; }
@Override
public boolean estaDisponible() { return disponible; }
@Override
public int comptarPlatsDisponibles() { return disponible ? 1 : 0; }
@Override
public void mostrar(int nivell) {
String sagnia = " ".repeat(nivell);
System.out.println(sagnia + nom + " ... " + preu + " €"
+ (disponible ? "" : " [EXHAURIT]"));
}
}El composite: una secció delega en els seus fills sense saber si són plats o subseccions:
public class SeccioCarta implements ComponentCarta {
private final String nom;
private final List<ComponentCarta> fills = new ArrayList<>();
public SeccioCarta(String nom) { this.nom = nom; }
public SeccioCarta afegir(ComponentCarta fill) {
fills.add(fill);
return this; // fluid, a l'estil del Builder del modul 2
}
public void treure(ComponentCarta fill) { fills.remove(fill); }
@Override public String getNom() { return nom; }
/** Una seccio esta disponible si ALGUN fill ho esta. */
@Override
public boolean estaDisponible() {
return fills.stream().anyMatch(ComponentCarta::estaDisponible);
}
/** Suma el que diguin els fills; la recursio es propaga sola. */
@Override
public int comptarPlatsDisponibles() {
return fills.stream().mapToInt(ComponentCarta::comptarPlatsDisponibles).sum();
}
@Override
public void mostrar(int nivell) {
System.out.println(" ".repeat(nivell) + "== " + nom + " ==");
for (ComponentCarta fill : fills) {
fill.mostrar(nivell + 1);
}
}
}Muntar l'arbre i usar-lo — el client no pregunta mai què té al davant:
SeccioCarta carta = new SeccioCarta("Carta La Bella Napoli");
SeccioCarta pizzes = new SeccioCarta("Pizzes");
pizzes.afegir(new SeccioCarta("Classiques")
.afegir(new Plat("Margarita", new BigDecimal("8.50")))
.afegir(new Plat("Prosciutto", new BigDecimal("9.90"))))
.afegir(new SeccioCarta("Especials")
.afegir(new Plat("Tartufo", new BigDecimal("13.50"))));
carta.afegir(pizzes);
carta.afegir(new SeccioCarta("Pastes").afegir(new Plat("Carbonara", new BigDecimal("10.50"))));
// Tractament uniforme: tant li fa passar-li un plat, una seccio o la carta sencera.
carta.mostrar(0);
System.out.println("Plats disponibles: " + carta.comptarPlatsDisponibles());Compara amb el doble bucle del primer intent: aquí no hi ha ni un sol if (esSeccio) en tot el sistema. La pregunta va desaparèixer perquè la interfície la va respondre d'una vegada per sempre: cada node sap operar-se a si mateix, i els grups saben delegar. Afegir una operació nova continua costant (un mètode més al Component i a cada classe), però afegir nivells o nodes a l'arbre costa zero codi.
Recorreguts recursius
Tota operació sobre l'arbre segueix el mateix motlle, que convé interioritzar perquè l'escriuràs moltes vegades:
- A la fulla: el cas base, resposta directa (un plat exhaurit compta 0).
- Al composite: combinar les respostes dels fills (
anyMatch,sum,allMatch, concatenar...), cridant la mateixa operació sobre cadascun. La recursió no es programa: emergeix de l'estructura.
El diagrama de seqüència de comptarPlatsDisponibles() sobre una carta petita ho fa visible:
sequenceDiagram
participant App
participant Carta as carta: SeccioCarta
participant Pizzes as pizzes: SeccioCarta
participant Marg as margarita: Plat
participant Aigua as aigua: Plat
App->>Carta: comptarPlatsDisponibles()
Carta->>Pizzes: comptarPlatsDisponibles()
Pizzes->>Marg: comptarPlatsDisponibles()
Marg-->>Pizzes: 1
Pizzes-->>Carta: 1
Carta->>Aigua: comptarPlatsDisponibles()
Aigua-->>Carta: 1
Carta-->>App: 2
L'app va fer una crida; l'estructura va fer la resta. Quan al mòdul 4 vegem Iterator i Visitor, tornarem sobre aquest arbre per recórrer-lo de maneres més sofisticades (només menció de moment: Iterator externalitza el recorregut; Visitor externalitza les operacions).
Menús: quan el grup també es ven
La carta agrupa per mostrar; els menús agrupen per vendre: "Menú Migdia" (primer + segon + postres, 12,90 € en lloc de la suma), i el "Menú Familiar" que conté dos menús migdia i una beguda gran. És un altre part-tot recursiu —menús dins de menús—, però amb una operació nova: el preu, calculat recursivament amb descompte:
public class Menu implements ComponentCarta {
private final String nom;
private final BigDecimal descompte; // p. ex. 0.15 = 15% sobre la suma
private final List<ComponentCarta> elements = new ArrayList<>();
public Menu(String nom, BigDecimal descompte) {
this.nom = nom;
this.descompte = descompte;
}
public Menu afegir(ComponentCarta element) { elements.add(element); return this; }
/** Preu recursiu: suma dels preus dels elements, amb descompte. */
public BigDecimal getPreu() {
BigDecimal suma = elements.stream()
.map(Menu::preuDe)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return suma.multiply(BigDecimal.ONE.subtract(descompte))
.setScale(2, RoundingMode.HALF_UP);
}
private static BigDecimal preuDe(ComponentCarta c) {
if (c instanceof Plat plat) return plat.getPreu();
if (c instanceof Menu menu) return menu.getPreu(); // recursio: menus niats
throw new IllegalStateException("Un menu no pot contenir seccions: " + c.getNom());
}
/** Un menu nomes es pot demanar si TOTS els seus elements estan disponibles. */
@Override
public boolean estaDisponible() {
return elements.stream().allMatch(ComponentCarta::estaDisponible);
}
@Override public String getNom() { return nom; }
@Override public int comptarPlatsDisponibles() {
return elements.stream().mapToInt(ComponentCarta::comptarPlatsDisponibles).sum();
}
@Override public void mostrar(int nivell) {
System.out.println(" ".repeat(nivell) + "★ " + nom + " ... " + getPreu() + " €");
elements.forEach(e -> e.mostrar(nivell + 1));
}
}Dos matisos amb suc:
- Fixa't en el canvi de política de disponibilitat: la secció usa
anyMatch(viva si queda alguna cosa per oferir) i el menú usaallMatch(si falten les postres, no hi ha menú). Mateix motlle recursiu, semàntica diferent per composite: el patró dóna l'estructura; el negoci, les regles. - Aquest
instanceofapreuDeés la costura honesta del disseny:getPreu()no és aComponentCartaperquè una secció no té preu ("Pizzes" no costa res: conté coses que costen). Podríem haver-lo pujat a la interfície i fer que les seccions llancessin una excepció... la qual cosa ens porta de dret al debat clàssic del patró.
Transparència davant de seguretat
Quines operacions van al Component? El GoF planteja el dilema amb els mètodes de gestió de fills (afegir/treure), i aplica igual a operacions com getPreu():
| Enfocament | Què va al Component | Guanyes | Pagues |
|---|---|---|---|
| Transparent | Tot, inclòs afegir/treure |
Uniformitat total: el client mai no fa instanceof ni cast |
Les fulles tenen mètodes absurds (plat.afegir(...)) que han de llançar UnsupportedOperationException: errors en execució |
| Segur | Només allò comú de debò; afegir/treure només al Composite |
El compilador impedeix plat.afegir(...): errors en compilació |
El client que construeix/modifica l'arbre necessita conèixer el tipus concret (o fer casts) |
La nostra implementació va triar l'enfocament segur (afegir viu a SeccioCarta i Menu, no a la interfície; getPreu viu a Plat i Menu), i és la recomanació general en Java modern: els errors de compilació són més barats que els d'execució, i a la pràctica qui construeix l'arbre gairebé sempre sap què està construint (es construeix des de configuració, base de dades o un builder), mentre que qui el recorre no necessita mutar res. La transparència total té el seu territori: frameworks on el client manipula arbres genèrics sense conèixer els tipus (el DOM, sistemes de fitxers). Tria per qui pateix: el que construeix (segur li és igual, transparent no li aporta) o el que recorre (en té prou amb allò comú)?
Quan usar-lo i quan no
Usa'l quan:
- El domini és un arbre part-tot: cartes amb seccions, menús niats, categories de restaurants, zones de repartiment amb subzones, grups de permisos... i vols operar igual sobre l'individu i sobre el grup.
- Detectes el símptoma del doble tractament: el mateix
if (esGrup) ... else ...repetint-se amb cada operació nova. - La profunditat de niada és arbitrària o creixerà: les llistes paral·leles (
List<Seccio>+List<Plat>) no escalen a això.
No l'usis quan:
- L'estructura és plana i continuarà sent-ho: una llista de plats sense seccions no necessita Component/Leaf/Composite; una
List<Plat>és més simple i més clara (KISS). - Fulles i grups no comparteixen operacions significatives: si la interfície comuna queda buida o plena d'
UnsupportedOperationException, el domini t'està dient que no hi ha tractament uniforme per capturar. - Necessites restriccions fortes sobre la forma de l'arbre (màxim dos nivells, tipus concrets per nivell): la uniformitat del Composite hi juga en contra; de vegades
Carta → List<Seccio> → List<Plat>tipat a mà és exactament el que vols.
Relació amb altres patrons (només menció): Decorator és estructuralment un Composite d'un sol fill i sol compartir interfície Component amb ell — ho veurem a la pròxima lliçó amb els extres dels plats; Iterator i Visitor són els patrons de recorregut i operació externa sobre aquestes estructures; els arbres es construeixen còmodament amb Builder i es clonen com a plantilles amb Prototype — de fet el RegistrePlantillesCarta del mòdul 2 desa exactament arbres com aquest.
Errors Comuns i Consells
- Retornar la llista de fills viva. Un
getFills()que retorna laListinterna permet a qualsevol mutar l'arbre per fora (carta.getFills().clear()). RetornaList.copyOf(fills)— la mateixa disciplina d'immutabilitat que vam aplicar al Builder de Comanda. - Cicles a l'"arbre". Res no impedeix
seccioA.afegir(seccioB)iseccioB.afegir(seccioA): la primera recursió s'ho menjarà com unStackOverflowError. Si l'arbre el munten dades externes (un panell d'administració de restaurant), valida en afegir (rebutjar si el nou fill conté el pare). - Referència al pare "per si de cas". Afegir
getPare()al Component sembla innocent i acobla cada node al seu context, complica moure subarbres i duplica la veritat estructural. Afegeix-la només amb un cas d'ús real (navegació ascendent) i mantén-la coherent aafegir/treure. - Ficar al Component allò que no és comú. Cada mètode que només té sentit en un tipus de node empeny cap a la transparència amb excepcions. Abans de pujar un mètode a la interfície, pregunta: què respon una fulla? i un grup? Si una de les dues respostes és "ni idea", a baix es queda.
- Recursió sense cas degenerat. Seccions buides:
estaDisponible()ambanyMatchsobre llista buida retornafalse(raonable), peròallMatchen unMenubuit retornatrue(menú de zero plats disponible!). Decideix i testeja els casos buits explícitament. - Consell: escriu els tests contra
ComponentCarta, no contra les classes concretes: un test que rep qualsevol subarbre i comprova invariants (comptar ≥ 0, mostrar no llança) documenta el tractament uniforme, que és la promesa del patró.
Exercicis
Exercici 1: plats aptes per a celíacs
Afegeix l'operació comptarSenseGluten() que retorna quants plats sense gluten hi ha sota qualsevol node. Suposa que Plat guanya un atribut booleà senseGluten. Escriu el mètode a les tres classes (Plat, SeccioCarta, Menu) i indica quin és el cas base i quin el pas recursiu.
Exercici 2: el menú impossible
La cuina exhaureix la Carbonara, que és l'únic "primer" del Menú Migdia; el Menú Familiar conté dos Menús Migdia i una beguda disponible. Sense executar codi, raona què retornen menuMigdia.estaDisponible() i menuFamiliar.estaDisponible(), i per què la propagació és automàtica.
Exercici 3: transparència amb conseqüències
Un company proposa pujar afegir(ComponentCarta) a la interfície ComponentCarta "per poder muntar arbres sense casts", fent que Plat.afegir llanci UnsupportedOperationException. Escriu (a) un exemple de codi que avui no compila i amb la seva proposta compilaria i fallaria en execució, i (b) un context en què la seva proposta seria, tot i això, la correcta.
Solucions
Solució 1:
// A Plat: CAS BASE, resposta directa
@Override
public int comptarSenseGluten() { return (disponible && senseGluten) ? 1 : 0; }
// A SeccioCarta: PAS RECURSIU, combinar fills
@Override
public int comptarSenseGluten() {
return fills.stream().mapToInt(ComponentCarta::comptarSenseGluten).sum();
}
// A Menu: mateix pas recursiu sobre els seus elements
@Override
public int comptarSenseGluten() {
return elements.stream().mapToInt(ComponentCarta::comptarSenseGluten).sum();
}I la firma s'afegeix a ComponentCarta (és genuïnament comuna: tota fulla i tot grup saben respondre-la). El motlle és sempre el mateix: fulla = cas base, composite = combinació de fills.
Solució 2: menuMigdia.estaDisponible() → la Carbonara retorna false, l'allMatch del menú troba un element no disponible → false. menuFamiliar.estaDisponible() → el seu allMatch pregunta a cada element; els dos Menús Migdia retornen false (per la recursió anterior) → false, encara que la beguda estigui disponible. Ningú no va escriure lògica de propagació "cap amunt": cada node va aplicar la seva regla local (allMatch) i la composició recursiva va fer emergir el resultat global. Un plat exhaurit a la fulla apaga menús a qualsevol profunditat.
Solució 3: (a) Avui ComponentCarta margarita = new Plat(...); margarita.afegir(unaAltraCosa); no compila (la interfície no té afegir): l'error es detecta gratis. Amb la proposta, compila i rebenta en execució amb UnsupportedOperationException — potser en producció, potser en un cas rar no testejat. (b) Seria raonable en un editor genèric de cartes (un panell d'administració que manipula nodes ComponentCarta arrossegant i deixant anar, sense conèixer tipus concrets): allà la uniformitat de manipulació val més que la seguretat estàtica, que és exactament el territori de la variant transparent.
Conclusió
Composite converteix una jerarquia part-tot en una estructura operable amb una sola interfície: fulles que responen directament, grups que combinen les respostes dels seus fills, i clients que fan una crida sense preguntar mai què tenen al davant. A PideYa va quedar instal·lat a la carta (ComponentCarta, Plat, SeccioCarta) i als menús niats (Menu, amb preu recursiu i la seva pròpia regla de disponibilitat), i va quedar decidida —amb criteri, no per inèrcia— la variant segura davant de la transparent.
Ara mira el plat que acabem de deixar a la carta: la Margarita. Un client la vol amb doble formatge; un altre, sense gluten; un altre, en ració gran amb doble formatge. Subclasses MargaritaDobleFormatge, MargaritaSenseGlutenDobleFormatge...? Això és una altra explosió combinatòria, i la resposta no és heretar sinó embolcallar: objectes que envolten el plat afegint-li preu i descripció, capa a capa, com qui vesteix una ceba a l'inrevés. Ens veiem a Decorator.
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
