Cada nit, PideYa genera l'informe de tancament diari de cada restaurant: es carreguen les comandes del dia, s'agreguen les xifres, es formata el resultat i es distribueix. El flux és sempre el mateix; el que canvia són passos concrets: l'informe PDF per a l'amo formata i distribueix diferent que el CSV per a comptabilitat o que el resum que s'envia per email. Strategy ens va ensenyar a intercanviar algorismes complets; aquí l'algorisme complet és comú i només varien peces. Per a això, el GoF reserva el seu patró d'herència per excel·lència: l'esquelet de l'algorisme viu en una classe base, fixat i intocable, i les subclasses omplen els buits — sota el principi de Hollywood: "no ens truquis; nosaltres et trucarem".
Contingut
- El problema a PideYa: informes que es copien el flux
- Intenció i estructura del patró
- Implementació Java completa
- Hooks: els passos opcionals
- El principi de Hollywood
- Ja has vist aquest patró (encara que no el vam anomenar)
- Template Method vs. Strategy: herència contra composició
- Quan usar-lo i quan no
- Relació amb altres patrons
- Errors comuns
- Exercicis i conclusió
El problema a PideYa: informes que es copien el flux
Avui existeixen InformePdfTancament i InformeCsvTancament, i són bessons amb copia-i-enganxa:
public class InformePdfTancament {
public void generar(String restaurantId, LocalDate dia) {
List<Comanda> comandes = repositori.comandesDelDia(restaurantId, dia); // igual
comandes = comandes.stream().filter(c -> c.getEstat() == LLIURADA).toList(); // igual
XifresTancament xifres = agregar(comandes); // igual
byte[] pdf = maquetarPdf(xifres); // ← diferent
magatzem.desar(restaurantId, dia, pdf); // ← diferent
}
// agregar(...) duplicat aqui...
}
public class InformeCsvTancament {
public void generar(String restaurantId, LocalDate dia) {
// ...les mateixes quatre primeres linies, copiades...
String csv = bolcarCsv(xifres); // ← diferent
sftp.pujar("comptabilitat/" + dia + ".csv", csv); // ← diferent
}
// ...i agregar(...) duplicat tambe
}El símptoma exacte: dos algorismes idèntics en estructura que només difereixen en passos concrets. Les conseqüències del copia-i-enganxa ja les coneixes: quan operacions demana "excloure també les comandes de prova", cal recordar-se de tocar totes les còpies del filtratge (i la tercera còpia, l'informe per email que ve de camí, neix ja dessincronitzada). El flux comú no té amo: viu duplicat a cada variant.
La inversió que ho arregla: el flux comú puja a una classe base com a mètode únic i intocable, i els passos variables baixen a les subclasses com a mètodes abstractes. La base crida les subclasses — no al revés.
Intenció i estructura del patró
Intenció (GoF): definir l'esquelet d'un algorisme en una operació, delegant alguns passos a les subclasses. Template Method permet que les subclasses redefineixin certs passos d'un algorisme sense canviar-ne l'estructura.
Aquest és un dels dos patrons de comportament d'àmbit de classe (ho vam anunciar a la introducció del mòdul): la relació es fixa amb herència, en compilar.
classDiagram
class InformeTancament {
<<abstract>>
+generar(restaurantId, dia) «final»
#carregarComandes(restaurantId, dia) List~Comanda~
#agregar(comandes) XifresTancament
#formatar(xifres)* byte[]
#distribuir(restaurantId, dia, contingut)*
#haDeGenerarSe(xifres) boolean «hook»
}
class InformePdfTancament {
#formatar(xifres) byte[]
#distribuir(restaurantId, dia, contingut)
}
class InformeCsvTancament {
#formatar(xifres) byte[]
#distribuir(restaurantId, dia, contingut)
}
class InformeEmailResum {
#formatar(xifres) byte[]
#distribuir(restaurantId, dia, contingut)
#haDeGenerarSe(xifres) boolean
}
InformeTancament <|-- InformePdfTancament
InformeTancament <|-- InformeCsvTancament
InformeTancament <|-- InformeEmailResum
| Rol GoF | A PideYa |
|---|---|
| AbstractClass (el mètode plantilla + passos primitius) | InformeTancament |
| ConcreteClass (implementa els passos variables) | InformePdfTancament, InformeCsvTancament, InformeEmailResum |
Els mètodes de la base es classifiquen en quatre tipus, i distingir-los és mitja lliçó:
| Tipus de mètode | Exemple | Les subclasses el toquen? |
|---|---|---|
| Mètode plantilla (l'esquelet) | generar(...) |
Mai — declara'l final |
| Pas concret (comú, implementat a la base) | carregarComandes, agregar |
No normalment |
| Pas abstracte (obligatori a cada subclasse) | formatar, distribuir |
L'han d'implementar |
| Hook (opcional, amb implementació per defecte) | haDeGenerarSe |
Poden, si volen |
Implementació Java completa
public abstract class InformeTancament {
protected final RepositoriComandes repositori;
protected InformeTancament(RepositoriComandes repositori) {
this.repositori = repositori;
}
/** EL METODE PLANTILLA: l'algorisme complet, en un sol lloc, final. */
public final void generar(String restaurantId, LocalDate dia) {
List<Comanda> comandes = carregarComandes(restaurantId, dia);
XifresTancament xifres = agregar(comandes);
if (!haDeGenerarSe(xifres)) { // hook: pas opcional
RegistreEsdeveniments.INSTANCIA.info("Tancament omes per a " + restaurantId);
return;
}
byte[] contingut = formatar(xifres); // pas abstracte
distribuir(restaurantId, dia, contingut); // pas abstracte
}
/** Pas concret: comu a tots els informes. Una sola copia del filtratge. */
protected List<Comanda> carregarComandes(String restaurantId, LocalDate dia) {
return repositori.comandesDelDia(restaurantId, dia).stream()
.filter(c -> c.getEstat() == EstatComanda.LLIURADA)
.toList();
}
/** Pas concret: l'agregacio, abans duplicada, ara amb amo unic. */
protected XifresTancament agregar(List<Comanda> comandes) {
BigDecimal total = comandes.stream()
.map(Comanda::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, Long> perPlat = comandes.stream()
.flatMap(c -> c.getLinies().stream())
.collect(groupingBy(LiniaComanda::getDescripcio, counting()));
return new XifresTancament(comandes.size(), total, perPlat);
}
/** Hook: per defecte, tot informe es genera. Les subclasses poden opinar. */
protected boolean haDeGenerarSe(XifresTancament xifres) {
return true;
}
/** Passos abstractes: cada informe HA DE dir com formata i distribueix. */
protected abstract byte[] formatar(XifresTancament xifres);
protected abstract void distribuir(String restaurantId, LocalDate dia, byte[] contingut);
}Les subclasses queden reduïdes a la seva diferència essencial:
public class InformeCsvTancament extends InformeTancament {
private final ClientSftp sftp;
public InformeCsvTancament(RepositoriComandes repo, ClientSftp sftp) {
super(repo);
this.sftp = sftp;
}
@Override
protected byte[] formatar(XifresTancament xifres) {
StringBuilder csv = new StringBuilder("plat;unitats\n");
xifres.perPlat().forEach((plat, n) -> csv.append(plat).append(';').append(n).append('\n'));
return csv.toString().getBytes(StandardCharsets.UTF_8);
}
@Override
protected void distribuir(String restaurantId, LocalDate dia, byte[] contingut) {
sftp.pujar("comptabilitat/" + restaurantId + "/" + dia + ".csv", contingut);
}
}Detalls d'ofici a la base, tots deliberats:
generarésfinal: l'esquelet és el contracte; si una subclasse el pogués redefinir, el patró s'evapora (i torna el copia-i-enganxa, ara amb herència).- Els passos són
protected: són la interfície cap a les subclasses, no cap al món. Ningú de fora no cridaformatar. - La correcció del filtratge ara és un sol canvi: "excloure comandes de prova" es toca a
carregarComandesi les tres variants ho hereten a l'instant.
Hooks: els passos opcionals
El hook haDeGenerarSe mereix la seva secció perquè és l'eina de flexibilitat fina del patró: un mètode amb implementació per defecte (sovint buida o trivial) que les subclasses poden sobreescriure per enganxar-se al flux sense estar-hi obligades. L'informe-resum per email l'usa: si el restaurant no va obrir, no hi ha res a enviar:
public class InformeEmailResum extends InformeTancament {
// ...
@Override
protected boolean haDeGenerarSe(XifresTancament xifres) {
return xifres.nombreComandes() > 0; // sense comandes, sense email
}
}Regles pràctiques per dissenyar hooks: pocs i amb nom que delati el moment (haDeGenerarSe, enAcabarDistribucio); per defecte inofensius (retornar el permissiu, no fer res); i documentats a la base — són el contracte d'extensió. Els frameworks els usen pertot arreu: els onCreate/onPause d'Android o els callbacks de cicle de vida de Spring són hooks de plantilles gegants.
El principi de Hollywood
"Don't call us, we'll call you." La inversió de control que defineix el patró: el codi de baix nivell (les subclasses) no crida el flux; és el flux (la base) qui crida les subclasses quan els toca. Compara-ho amb una llibreria clàssica: tu crides Math.max(...) quan vols; en Template Method, InformeCsvTancament mai no decideix quan formatar — és cridat per generar en el moment just.
Aquesta inversió és la llavor d'una cosa que ja uses: els frameworks funcionen així. Spring, JUnit o Android defineixen el flux (el mètode plantilla, a escala industrial) i el teu codi omple buits que el framework invoca. La diferència entre usar una llibreria i usar un framework és el principi de Hollywood — i l'has après aquí, en un patró d'una classe i mitja.
Ja has vist aquest patró (encara que no el vam anomenar)
Dos retrobaments del curs mateix:
- El
gestionar(...)del Chain of Responsibility: mètode públic que fixa el protocol (comprovar → tallar o delegar) cridant elcomprovarabstracte de cada baula. Llavors en vam dir "truc de plantilla"; era exactament aquest patró en miniatura. - El flux comú de les
Notificaciodel Bridge admet la mateixa jugada: unenviar()final a l'abstracció que fa validar → compondre → enviar pel canal, ambcompondre()abstracte per tipus de notificació. Template Method dins del costat esquerre d'un Bridge: els patrons s'imbriquen.
I al JDK: AbstractList et dona add, contains o indexOf gratis si implementes get i size (plantilles sobre primitives), i els tests amb @BeforeEach/@Test/@AfterEach de JUnit són un mètode plantilla que tu no veus i que crida els teus mètodes.
Template Method vs. Strategy: herència contra composició
L'acarament promès a la lliçó anterior — mateix problema (variar el "com" sense tocar els clients), solucions a les dues escoles:
| Criteri | Template Method | Strategy |
|---|---|---|
| Mecanisme | Herència (àmbit de classe) | Composició (àmbit d'objecte) |
| Què varia | Passos d'un algorisme l'esquelet del qual és fix | L'algorisme sencer |
| Quan es decideix la variant | En compilar/instanciar la subclasse | En execució; intercanviable en calent |
| Combinar variants de diversos eixos? | Malament: una subclasse per combinació | Bé: injecta una estratègia per eix |
| Accés a l'estat comú | Directe (camps protected de la base) |
Només el que la interfície passa com a paràmetres |
| Risc característic | Jerarquies rígides, acoblament base-subclasse | Més peces i cablejat d'injecció |
| A PideYa | InformeTancament |
CalculEnviament, EstrategiaAssignacio |
La guia de decisió, honesta: si els passos variables necessiten compartir molt context del flux i les variants són poques i estables, Template Method és directe i concís. Si necessites canviar la variant en execució, combinar eixos de variació o testejar les variants aïllades del flux, Strategy — de fet, l'evolució típica és començar amb Template Method i, quan les subclasses es multipliquen per combinatòria, extreure cada pas variable com a estratègia injectada ("composició per sobre d'herència" té la seva història des del mòdul 1, i aquesta és la seva batalla clàssica). Tots dos conviuen bé: un mètode plantilla els passos del qual deleguen en estratègies.
Quan usar-lo i quan no
Usa'l quan:
- Diverses classes implementen el mateix algorisme amb passos diferents i el copia-i-enganxa ja fa mal (o en farà: tercera variant a la vista).
- Vols fixar l'ordre del flux com a contracte inviolable i oferir punts d'extensió controlats (passos i hooks).
- Estàs construint una base reutilitzable o miniframework intern on altres equips omplen buits.
Evita'l quan:
- Les variants necessiten canviar en execució o combinar-se: l'herència ho fixa tot en construir — Strategy.
- La "part comuna" és petita o forçada: una base artificial per compartir dues línies acobla jerarquies senceres per no res; de vegades un mètode estàtic compartit és la resposta avorrida i correcta.
- Ja hi ha una jerarquia per un altre motiu: l'herència és un recurs únic en Java; gastar-la en el flux d'informes impedeix usar-la per a una altra cosa.
Relació amb altres patrons
- Strategy: la taula de dalt; passos de plantilla extraïbles com a estratègies.
- Factory Method: GoF el defineix literalment com "una especialització de Template Method" on el pas variable és crear un objecte — el nostre vell conegut del mòdul 2, reenquadrat.
- Chain of Responsibility i Bridge: els retrobaments de la secció anterior.
- Builder: el director d'un builder executa passos de construcció en ordre fix — esperit de plantilla al món creacional.
Errors comuns
- Mètode plantilla no-final: una subclasse el redefineix "només per a aquest cas" i l'esquelet únic es fragmenta.
finalno és paranoia; és el patró. - Massa passos abstractes: si la base ho delega tot, no hi ha plantilla — només una interfície cara. La base ha de posseir el flux i la major part de la feina comuna.
- Hooks que les subclasses estan obligades a cridar (
super.enAcabar()"o es trenca tot"): el contracte fràgil clàssic de l'herència. Dissenya la base perquè l'obligatori visqui a la plantilla, no a la bona memòria de qui hereta. - Subclasses que se salten el flux cridant els passos directament (
formatardes de fora degenerar): visibilitatprotectedi disciplina; els passos no són API pública. - Estat mutable
protectedcompartit a mans plenes: les subclasses acaben depenent de les tripes de la base i cada refactor de la base trenca fills. Passa dades com a paràmetres dels passos (com el nostreXifresTancament) sempre que puguis.
Exercicis
Exercici 1: variant nova en deu línies
Implementa InformeJsonTancament: formata les xifres com a JSON (usa el format que vulguis) i les publica a magatzemDatalake.publicar("tancaments/" + dia + "/" + restaurantId + ".json", contingut). Quantes línies de flux (carregar/filtrar/agregar) has escrit?
Exercici 2: el hook de després
Operacions vol que, després de distribuir amb èxit, cada informe pugui opcionalment registrar mètriques (el PDF no; el CSV ha d'anotar metriques.registrarEnviamentComptabilitat(dia)). Afegeix el hook a la base (nom, firma, implementació per defecte, punt de crida a generar) i usa'l a InformeCsvTancament.
Exercici 3: plantilla o estratègia?
L'equip de notificacions vol unificar el flux validar destinatari → compondre missatge → enviar → registrar: (a) si les variants són "confirmació", "retard" i "promoció", que difereixen només en compondre, què usaries?; (b) i si a més l'enviament ha de poder canviar en execució entre push/SMS/email segons preferències de l'usuari? Relaciona la teva resposta amb el que es va construir al Bridge del mòdul 3.
Solucions
Solució 1:
public class InformeJsonTancament extends InformeTancament {
private final MagatzemDatalake magatzemDatalake;
public InformeJsonTancament(RepositoriComandes repo, MagatzemDatalake magatzem) {
super(repo);
this.magatzemDatalake = magatzem;
}
@Override
protected byte[] formatar(XifresTancament xifres) {
String json = "{\"comandes\":" + xifres.nombreComandes()
+ ",\"total\":" + xifres.total() + "}";
return json.getBytes(StandardCharsets.UTF_8);
}
@Override
protected void distribuir(String restaurantId, LocalDate dia, byte[] contingut) {
magatzemDatalake.publicar("tancaments/" + dia + "/" + restaurantId + ".json", contingut);
}
}Línies de flux escrites: zero — carregar, filtrar i agregar venen heretades i amb les seves correccions futures incloses. Aquest és el dividend del patró.
Solució 2: a la base:
/** Hook: cridat despres d'una distribucio completada. Per defecte, res. */
protected void enDistribuirAmbExit(String restaurantId, LocalDate dia, XifresTancament xifres) {
}i al mètode plantilla, l'última línia de generar passa a ser:
distribuir(restaurantId, dia, contingut);
enDistribuirAmbExit(restaurantId, dia, xifres); // el flux crida el hook (Hollywood)A InformeCsvTancament:
@Override
protected void enDistribuirAmbExit(String restaurantId, LocalDate dia, XifresTancament xifres) {
metriques.registrarEnviamentComptabilitat(dia);
}El PDF no toca res: els hooks no obliguen. (I fixa't en l'ordre: el hook va després de distribuir a la plantilla, no dins de cada distribuir — el moment de crida és propietat de l'esquelet.)
Solució 3: (a) Template Method: esquelet fix, un sol pas variable (compondre), variants estables — plantilla amb compondre() abstracte, exactament la jugada esbossada a la secció de retrobaments; (b) l'eix del canal ha de canviar en execució i per usuari: això no ho pot fixar l'herència — i és just el que el Bridge ja va resoldre: l'abstracció Notificacio (on pot viure el mètode plantilla amb compondre variable per herència) delega l'enviament en un CanalEnviament compost i intercanviable. És a dir: la resposta correcta és tots dos, cadascun al seu eix — plantilla per al flux i els seus passos per tipus, composició per al canal. Els eixos de variació independents demanen composició; els passos d'un flux fix toleren herència.
Conclusió
Template Method posa el flux comú sota un amo únic: InformeTancament.generar(...) fixa l'esquelet — carregar, agregar, generar?, formatar, distribuir — com a mètode final, la lògica compartida viu una sola vegada, i cada variant (PDF, CSV, email, i el JSON de l'exercici) aporta només la seva diferència, invocada per la base quan toca: Hollywood en estat pur, la mateixa inversió de control amb què et parlen els frameworks que uses cada dia. I ha quedat traçada la tercera frontera de Strategy: passos d'un flux fix → herència i plantilla; algorismes complets, combinables o canviants en calent → composició i estratègies.
Queda un últim patró al catàleg de comportament, i ataca el problema més retorçat de tots: afegir operacions noves a una jerarquia estable sense tocar-la. La carta Composite porta dos mòduls acumulant pretendents — exportar a JSON, calcular al·lèrgens, auditar preus — i ficar cada operació dins de Plat i SeccioCarta les convertiria en calaixos de sastre. La solució exigeix el truc tècnic més elegant del GoF: el double dispatch. Ens veiem a Visitor.
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
