La carta de La Bella Napoli va quedar modelada com un arbre, però el client no demana "la Margarita de la carta": demana la seva Margarita — amb doble formatge, sense gluten, en ració gran. Cada extra modifica el preu i la descripció, els extres es combinen lliurement i demà hi haurà extres nous. Modelar-ho amb subclasses és condemnar-se a l'explosió combinatòria; modelar-ho amb banderes booleanes és condemnar la classe a créixer sense fi. Decorator ofereix la tercera via: embolcallar l'objecte amb capes que afegeixen responsabilitats, componibles en execució, sense tocar ni heretar la classe original. I de regal veurem que el mateix truc resol un problema tècnic pendent del mòdul 2: afegir reintents i logging als notificadors.
Contingut
- El problema a PideYa: els extres del plat
- Per què l'herència no pot amb això
- Intenció i estructura del patró
- Implementació Java completa
- Decoradors tècnics: reintents i logging sobre
Notificador - Decorator al JDK: els streams de java.io
- Quan usar-lo i quan no
- Errors comuns, exercicis i conclusió
El problema a PideYa: els extres del plat
Quan un client afegeix una Margarita a la seva comanda, pot personalitzar-la:
| Extra | Efecte en el preu | Efecte en la descripció |
|---|---|---|
| Doble formatge | +1,50 € | "... amb doble formatge" |
| Adaptació sense gluten | +2,00 € | "... (sense gluten)" |
| Ració gran | +30% sobre l'acumulat | "... ració gran" |
Els extres es combinen en qualsevol subconjunt (i el "+30%" fa que fins i tot importi l'ordre d'aplicació). El que viatja a cada LiniaComanda de la Comanda que vam construir al mòdul 2 ha de respondre dues preguntes: quina descripció imprimeixo al tiquet? quin preu cobro? És a dir, en el domini de la comanda un plat personalitzat és un Producte:
El Plat de la carta de la lliçó anterior implementa Producte trivialment (el seu nom, el seu preu base). El problema són les combinacions.
Per què l'herència no pot amb això
Intent 1: subclasses. MargaritaDobleFormatge, MargaritaSenseGluten, MargaritaDobleFormatgeSenseGluten, MargaritaDobleFormatgeSenseGlutenGran... Amb 3 extres opcionals hi ha 2³ = 8 combinacions per plat; amb 5 extres, 32. I tot es duplica per a la Prosciutto, la Carbonara... L'herència fixa les combinacions en compilació, quan els extres es decideixen en execució, client a client. És l'explosió multiplicativa que ja vam olorar a Bridge, agreujada: allà eren dos eixos; aquí, un eix per cada extra.
Intent 2: banderes a la classe. Plat amb boolean dobleFormatge, senseGluten, racioGran i un getPreu() ple d'if. Funciona... fins que màrqueting inventa l'extra "bacó" i cal modificar Plat (adéu OCP), i la classe acumula responsabilitats de tots els extres presents i futurs (adéu SRP). A més cada plat carrega camps que el 90% de les comandes no usa.
La necessitat, formulada amb precisió: afegir responsabilitats (preu extra, text extra) a objectes individuals, no a classes, en qualsevol combinació, decidida en temps d'execució, i sense modificar el codi existent.
Intenció i estructura del patró
Intenció (GoF): assignar responsabilitats addicionals a un objecte dinàmicament. Els decoradors ofereixen una alternativa flexible a l'herència per estendre funcionalitat.
El mecanisme té una simetria preciosa: el decorador implementa la mateixa interfície que decora i conté un objecte d'aquesta interfície. És alhora un Producte (cap enfora) i un usuari de Producte (cap endins). Per això els decoradors s'apilen: cada capa embolcalla l'anterior sense saber si embolcalla l'objecte base o una altra capa.
classDiagram
class Producte {
<<interface>>
+getDescripcio() String
+getPreu() BigDecimal
}
class Plat {
+getDescripcio() String
+getPreu() BigDecimal
}
class ExtraPlat {
<<abstract>>
#decorat: Producte
+ExtraPlat(decorat: Producte)
+getDescripcio() String
+getPreu() BigDecimal
}
class ExtraDobleFormatge {
+getDescripcio() String
+getPreu() BigDecimal
}
class AdaptacioSenseGluten {
+getDescripcio() String
+getPreu() BigDecimal
}
class ExtraRacioGran {
+getDescripcio() String
+getPreu() BigDecimal
}
Producte <|.. Plat
Producte <|.. ExtraPlat
ExtraPlat <|-- ExtraDobleFormatge
ExtraPlat <|-- AdaptacioSenseGluten
ExtraPlat <|-- ExtraRacioGran
ExtraPlat o-- Producte : embolcalla
| Rol GoF | A PideYa |
|---|---|
| Component (interfície comuna) | Producte |
| ConcreteComponent (l'objecte base) | Plat |
| Decorator (base abstracta amb la referència) | ExtraPlat |
| ConcreteDecorator | ExtraDobleFormatge, AdaptacioSenseGluten, ExtraRacioGran |
Fixa't en les dues fletxes d'ExtraPlat: implementa Producte i conté un Producte. Aquesta doble relació amb la mateixa interfície és la firma visual del patró (compara-la amb Composite: allà el grup contenia molts Components; el decorador en conté exactament un — el GoF anomena el Decorator "un composite degenerat amb un sol fill").
Implementació Java completa
El component concret ja el tenim: Plat de la carta, que implementa Producte retornant el seu nom i el seu preu base.
El decorador base concentra la lampisteria (desar la referència, delegar per defecte):
public abstract class ExtraPlat implements Producte {
protected final Producte decorat;
protected ExtraPlat(Producte decorat) {
this.decorat = Objects.requireNonNull(decorat);
}
// Per defecte, delegar: cada decorador concret sobreescriu el que altera.
@Override public String getDescripcio() { return decorat.getDescripcio(); }
@Override public BigDecimal getPreu() { return decorat.getPreu(); }
}Els decoradors concrets — cadascun és minúscul, fa una sola cosa (SRP) i crida sempre l'embolcallat:
public class ExtraDobleFormatge extends ExtraPlat {
private static final BigDecimal RECARREC = new BigDecimal("1.50");
public ExtraDobleFormatge(Producte decorat) { super(decorat); }
@Override
public String getDescripcio() { return decorat.getDescripcio() + " amb doble formatge"; }
@Override
public BigDecimal getPreu() { return decorat.getPreu().add(RECARREC); }
}
public class AdaptacioSenseGluten extends ExtraPlat {
private static final BigDecimal RECARREC = new BigDecimal("2.00");
public AdaptacioSenseGluten(Producte decorat) { super(decorat); }
@Override
public String getDescripcio() { return decorat.getDescripcio() + " (sense gluten)"; }
@Override
public BigDecimal getPreu() { return decorat.getPreu().add(RECARREC); }
}
public class ExtraRacioGran extends ExtraPlat {
private static final BigDecimal FACTOR = new BigDecimal("1.30");
public ExtraRacioGran(Producte decorat) { super(decorat); }
@Override
public String getDescripcio() { return decorat.getDescripcio() + ", racio gran"; }
@Override
public BigDecimal getPreu() {
return decorat.getPreu().multiply(FACTOR).setScale(2, RoundingMode.HALF_UP);
}
}El client compon en execució, capa a capa, segons el que marqui l'usuari a l'app:
Producte comanda = new Plat("Margarita", new BigDecimal("8.50"));
comanda = new ExtraDobleFormatge(comanda);
comanda = new AdaptacioSenseGluten(comanda);
comanda = new ExtraRacioGran(comanda);
System.out.println(comanda.getDescripcio());
// Margarita amb doble formatge (sense gluten), racio gran
System.out.println(comanda.getPreu());
// (8.50 + 1.50 + 2.00) * 1.30 = 15.60Cada crida a getPreu() travessa la ceba de fora endins i el resultat es construeix de dins enfora:
sequenceDiagram
participant App
participant Gran as :ExtraRacioGran
participant SenseGluten as :AdaptacioSenseGluten
participant Formatge as :ExtraDobleFormatge
participant Marg as :Plat
App->>Gran: getPreu()
Gran->>SenseGluten: getPreu()
SenseGluten->>Formatge: getPreu()
Formatge->>Marg: getPreu()
Marg-->>Formatge: 8.50
Formatge-->>SenseGluten: 10.00
SenseGluten-->>Gran: 12.00
Gran-->>App: 15.60
El compte final del patró: 3 classes cobreixen les 8 combinacions (i les 16 quan arribi el bacó: una classe més). L'extra nou no toca res d'existent (OCP), cada extra viu a la seva classe (SRP), i com que tot és un Producte, la LiniaComanda del mòdul 2 el desa sense assabentar-se de quantes capes porta. Fixa't també que l'ordre importa i el model ho expressa: ració gran aplicada l'última multiplica els extres; aplicada la primera, només el preu base. Això, que amb banderes booleanes era impossible d'expressar, aquí és simplement l'ordre d'embolcall.
Decoradors tècnics: reintents i logging sobre Notificador
El mateix patró brilla lluny de la carta. Pendent des del mòdul 2: els enviaments de notificacions fallen de vegades (xarxes, proveïdors caiguts) i volem reintents i traces a RegistreEsdeveniments... sense tocar NotificadorPush, NotificadorSms ni NotificadorEmail, i sense duplicar aquesta lògica als tres. Decoradors sobre la interfície Notificador:
public class NotificadorAmbReintents implements Notificador {
private final Notificador decorat;
private final int maxIntents;
public NotificadorAmbReintents(Notificador decorat, int maxIntents) {
this.decorat = decorat;
this.maxIntents = maxIntents;
}
@Override
public void enviar(String destinatari, String missatge) {
NotificacioException ultima = null;
for (int intent = 1; intent <= maxIntents; intent++) {
try {
decorat.enviar(destinatari, missatge);
return; // exit: sortim
} catch (NotificacioException e) {
ultima = e; // fallada: seguent intent
}
}
throw new NotificacioException("Exhaurits " + maxIntents + " intents", ultima);
}
}
public class NotificadorAmbLogging implements Notificador {
private final Notificador decorat;
public NotificadorAmbLogging(Notificador decorat) { this.decorat = decorat; }
@Override
public void enviar(String destinatari, String missatge) {
RegistreEsdeveniments.INSTANCE.info("Enviant a " + destinatari);
try {
decorat.enviar(destinatari, missatge);
RegistreEsdeveniments.INSTANCE.info("Enviat OK a " + destinatari);
} catch (NotificacioException e) {
RegistreEsdeveniments.INSTANCE.error("Fallada enviant a " + destinatari + ": " + e.getMessage());
throw e;
}
}
}Composició típica (i observa que l'ordre torna a significar coses: logging per fora dels reintents traça el resultat final; per dins, cada intent):
Notificador notificador =
new NotificadorAmbLogging(
new NotificadorAmbReintents(
new NotificadorSms(), 3));I la connexió amb els creacionals és directa: el RegistreNotificadors de Suppliers pot registrar el canal ja decorat (() -> new NotificadorAmbLogging(new NotificadorAmbReintents(new NotificadorSms(), 3))), de manera que cap client no sàpiga mai quantes capes porta el seu notificador. Aquesta família de decoradors tècnics (reintents, logging, mètriques, cache, xifratge) és de les més rendibles del patró en sistemes reals — amb un parent proper, Proxy, del qual el distingirem a la seva lliçó: mateixa mecànica, una altra intenció.
Decorator al JDK: els streams de java.io
L'exemple canònic del patró és a Java des del 1996. Tota l'E/S està construïda com a decoradors sobre InputStream/OutputStream:
InputStream entrada =
new BufferedInputStream( // decorador: afegeix buffering
new GZIPInputStream( // decorador: afegeix descompressio
new FileInputStream("comandes-2026.csv.gz"))); // component concretBufferedInputStream i GZIPInputStream implementen InputStream i embolcallen un InputStream (via la base FilterInputStream, que és literalment el rol Decorator): la doble relació exacta del nostre ExtraPlat. Les combinacions (fitxer comprimit amb buffer, socket xifrat sense buffer...) serien inviables per herència; per decoració són una línia. A la mateixa família: Collections.unmodifiableList(...) i Collections.synchronizedList(...) decoren una List afegint-hi immutabilitat o sincronització. Quan encadenis constructors que reben "el mateix que retornen", estàs decorant.
Quan usar-lo i quan no
Usa'l quan:
- Hi ha responsabilitats opcionals i combinables sobre un objecte (extres de plats; buffering/compressió/xifratge sobre streams) i les combinacions es decideixen en execució.
- Vols afegir comportament transversal (reintents, traces, mètriques) a implementacions existents sense modificar-les ni duplicar-lo a cadascuna.
- L'herència és impracticable: explosió combinatòria, o la classe és
final, o vols decorar instàncies concretes i no totes.
No l'usis quan:
- Només hi ha una variació i no es combina amb res: una subclasse o un paràmetre és més simple (KISS). El patró cobra sentit a partir de la combinatòria.
- Necessites treure o consultar capes amb freqüència: la ceba és opaca (no hi ha manera neta de preguntar "porta doble formatge?" des de fora) i desembolcallar no és al contracte. Si la comanda necessita llistar els seus extres per a la cuina, potser el model correcte són dades (una llista d'extres amb regles de preu) i no decoradors — modelar amb objectes no sempre guanya a modelar amb dades.
- La interfície Component és enorme: cada decorador l'ha d'implementar sencera, i vint mètodes de delegació per alterar-ne un és mal senyal (revisa ISP abans que Decorator).
- La identitat importa: l'objecte decorat té una altra identitat (
==,equals) que l'original, la qual cosa trenca caches i comparacions ingènues.
Relació amb altres patrons (només menció): Composite comparteix interfície i filosofia — decoradors i composites conviuen d'allò més bé: pots decorar una fulla de l'arbre; Proxy té idèntica estructura amb intenció de control d'accés en lloc d'afegir responsabilitats (acarament a la seva lliçó i a la comparativa); Adapter també embolcalla però canviant la interfície; Strategy canvia les tripes de l'objecte mentre Decorator li canvia la pell; i les piles de decoradors es munten còmodament amb les fàbriques del mòdul 2.
Errors Comuns i Consells
- Trencar la delegació. Un decorador que oblida cridar
decorat.metode()en algun mètode talla la cadena: les capes interiors deixen d'executar-se silenciosament. La base abstracta que delega per defecte (ExtraPlat) existeix just per a això. - Decoradors amb coneixement mutu. Si
ExtraRacioGranfainstanceof ExtraDobleFormatgeper ajustar el seu càlcul, la independència de capes ha mort i l'ordre d'embolcall es torna un camp de mines. Cada decorador només coneix la interfície. - Estat compartit amb el decorat. Un decorador que fa cache del
getPreu()de l'embolcallat es desincronitza si el plat canvia. Decoradors sense estat (o amb estat propi immutable) dormen millor. equals/hashCodeheretats alegrement. Una Margarita amb formatgeequalsuna Margarita? Decideix la semàntica explícitament; per defecte, identitats diferents, i documentat.- La ceba infinita al depurador. Deu capes niades fan penós el debugging (deu frames per a un
getPreu()). És un cost real del patró: mantén les piles curtes i amb noms clars; si eltoString()de cada capa s'autodescriu, el depurador es torna llegible. - Consell: quan la composició de capes es repeteixi (tot notificador de producció porta logging + reintents), no l'escampis pel codi: dóna-li nom en un sol lloc — una fàbrica, o el mateix registre de Suppliers. Compondre és barat; compondre igual pertot arreu a mà és una duplicació disfressada.
Exercicis
Exercici 1: l'extra de bacó i el descompte de la casa
Escriu (a) ExtraBacon (+1,20 €, descripció "... amb baco"); (b) DescompteDeLaCasa, un decorador que aplica un percentatge de descompte al preu acumulat i afegeix " [descompte X% aplicat]" a la descripció. Importa en quina posició de la ceba s'aplica el descompte? On el col·locaries i per què?
Exercici 2: llegir la ceba
Donat aquest codi, escriu la descripció i el preu resultants, justificant l'ordre dels càlculs:
Producte p = new ExtraRacioGran(
new ExtraDobleFormatge(
new Plat("Prosciutto", new BigDecimal("9.90"))));Exercici 3: decorator o no?
Per a cada necessitat, digues si Decorator és adequat i, si no, quina alternativa simple usaries: (1) que totes les notificacions de PideYa, sempre, incloguin el prefix "[PideYa]"; (2) mesurar la durada de les crides a PassarellaPagament només a l'entorn de proves de rendiment; (3) que la cuina pugui llistar els extres d'un plat per preparar-lo.
Solucions
Solució 1:
public class ExtraBacon extends ExtraPlat {
private static final BigDecimal RECARREC = new BigDecimal("1.20");
public ExtraBacon(Producte decorat) { super(decorat); }
@Override public String getDescripcio() { return decorat.getDescripcio() + " amb baco"; }
@Override public BigDecimal getPreu() { return decorat.getPreu().add(RECARREC); }
}
public class DescompteDeLaCasa extends ExtraPlat {
private final BigDecimal percentatge; // p. ex. 10 = 10%
public DescompteDeLaCasa(Producte decorat, BigDecimal percentatge) {
super(decorat);
this.percentatge = percentatge;
}
@Override public String getDescripcio() {
return decorat.getDescripcio() + " [descompte " + percentatge + "% aplicat]";
}
@Override public BigDecimal getPreu() {
BigDecimal factor = BigDecimal.ONE.subtract(
percentatge.movePointLeft(2));
return decorat.getPreu().multiply(factor).setScale(2, RoundingMode.HALF_UP);
}
}Sí que importa: el descompte només descompta el que té a dins. Aplicat al mig de la ceba, els extres exteriors quedarien sense descomptar. Ha de ser la capa més externa per descomptar sobre el total — i com que aquesta regla és de negoci, millor blindar-la on es compon (la fàbrica/servei que munta el producte), no confiar-la a la memòria de cada programador.
Solució 2: de dins enfora: base 9,90 → ExtraDobleFormatge: 9,90 + 1,50 = 11,40 → ExtraRacioGran: 11,40 × 1,30 = 14,82 €. Descripció, també de dins enfora: "Prosciutto" → "Prosciutto amb doble formatge" → "Prosciutto amb doble formatge, racio gran". La ració gran, en ser la capa externa, multiplica també el recàrrec del formatge — coherent amb la política de la carta (una ració gran porta més formatge).
Solució 3: (1) No: si és sempre i per a tothom, no hi ha combinatòria ni opcionalitat; el prefix pertany al codi comú (la base o el punt únic d'enviament). Decorator per a l'invariable és cerimònia. (2) Sí: responsabilitat transversal, opcional (només un entorn), sense tocar les passarel·les: un PassarellaAmbMetriques implements PassarellaPagament que es compon únicament en aquell entorn — de fet és a la frontera amb Proxy, com veurem. (3) No amb decoradors purs: la ceba és opaca i desembolcallar-la amb instanceof és lluitar contra el patró. La cuina necessita dades estructurades (llista d'extres), senyal que aquell cas demana modelar els extres com a dades a més de (o en lloc de) com a capes.
Conclusió
Decorator substitueix la pregunta "quina subclasse necessito per a aquesta combinació?" per "quines capes embolcallo avui?": una interfície comuna, un objecte base i decoradors minúsculs que afegeixen la seva responsabilitat i deleguen la resta. A PideYa van quedar els extres componibles sobre Producte (ExtraDobleFormatge, AdaptacioSenseGluten, ExtraRacioGran) i els decoradors tècnics sobre Notificador (NotificadorAmbReintents, NotificadorAmbLogging), i de passada ja saps llegir l'E/S de Java com el que sempre va ser: una ceba de manual.
Hem après a traduir interfícies, estendre ponts, muntar arbres i embolcallar capes. Però mentrestant, al checkout de PideYa, l'app mòbil continua fent malabars: validar la cistella, demanar els impostos a la família del mercat, cobrar amb la passarel·la, construir la comanda, notificar... cinc subsistemes coordinats a mà des de cada client. De vegades el millor servei que pot prestar un disseny no és una estructura enginyosa, sinó una porta simple davant de la complexitat. Ens veiem a Facade.
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
