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

  1. El problema a PideYa: informes que es copien el flux
  2. Intenció i estructura del patró
  3. Implementació Java completa
  4. Hooks: els passos opcionals
  5. El principi de Hollywood
  6. Ja has vist aquest patró (encara que no el vam anomenar)
  7. Template Method vs. Strategy: herència contra composició
  8. Quan usar-lo i quan no
  9. Relació amb altres patrons
  10. Errors comuns
  11. 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 és final: 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 crida formatar.
  • La correcció del filtratge ara és un sol canvi: "excloure comandes de prova" es toca a carregarComandes i 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 el comprovar abstracte de cada baula. Llavors en vam dir "truc de plantilla"; era exactament aquest patró en miniatura.
  • El flux comú de les Notificacio del Bridge admet la mateixa jugada: un enviar() final a l'abstracció que fa validar → compondre → enviar pel canal, amb compondre() 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. final no é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 (formatar des de fora de generar): visibilitat protected i disciplina; els passos no són API pública.
  • Estat mutable protected compartit 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 nostre XifresTancament) 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

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