Aquesta lliçó salda tots els deutes del curs.

Des del mòdul 3 has anat escrivint patrons de disseny sense anomenar-los pel seu nom. A 03-04, en veure constructors amb massa paràmetres, es va prometre un Builder «per a més endavant». A 04-02, en parlar de classes abstractes amb un mètode plantilla, es va dir «això té nom, i el veurem». A 07-03, els fluxos d'E/S embolcallats els uns dins dels altres eren «un patró que estudiarem». A 10-03 vas escriure proxies dinàmics complets sense dir que allò era el patró Proxy. A 11-02, la injecció de dependències era «la D d'un conjunt de principis que veurem al final». Aquest final és ara.

Però abans, un advertiment que importa més que el catàleg sencer.

Els patrons no són objectius. Ningú no s'hauria de proposar «fer servir el patró Observador» de la mateixa manera que ningú no es proposa «fer servir un bucle while». Un patró és una resposta reconeguda a un problema recurrent, i aplicar-lo sense tenir el problema produeix exactament el mateix dany que no aplicar-lo quan el tens: codi més difícil d'entendre, amb més peces i més indirecció, sense cap avantatge a canvi. El sobredisseny és un problema tan real com la manca de disseny, i en projectes joves és més freqüent.

La utilitat real de conèixer els patrons és doble, i cap de les dues és «aplicar-los». La primera és vocabulari: dir «CatalegAmbCache és un Decorador del repositori» estalvia tres minuts d'explicació i transmet exactament l'estructura i les seves conseqüències. La segona és reconeixement: quan un problema et resulta incòmode, saber que algú ja el va resoldre et dona un punt de partida en lloc d'un full en blanc.

En acabar aquesta lliçó coneixeràs els principis SOLID amb exemples reals de BiblioTech abans i després, dominaràs els patrons GoF que de debò es fan servir en Java modern, sabràs implementar-los i —sobretot— sabràs quan no fer-ho, reconeixeràs els patrons arquitectònics que Spring i JPA apliquen per tu, i identificaràs els antipatrons més comuns, inclòs un que BiblioTech té i que discutirem honestament.

Contingut

  1. Què és un patró i què no
  2. El catàleg GoF i el seu context històric
  3. Principis abans que patrons
  4. SOLID: responsabilitat única (S)
  5. SOLID: obert/tancat (O)
  6. SOLID: substitució de Liskov (L)
  7. SOLID: segregació d'interfícies (I)
  8. SOLID: inversió de dependències (D)
  9. DRY, KISS, YAGNI i la llei de Demeter
  10. Patrons creacionals: Singleton
  11. Patrons creacionals: Factory Method i Abstract Factory
  12. Patrons creacionals: Builder
  13. Patrons creacionals: Prototype
  14. Patrons estructurals: Adaptador
  15. Patrons estructurals: Decorador
  16. Patrons estructurals: Proxy
  17. Patrons estructurals: Facade i Composite
  18. Patrons de comportament: Estratègia
  19. Patrons de comportament: Template Method
  20. Patrons de comportament: Observador
  21. Patrons de comportament: Estat
  22. Patrons de comportament: Ordre
  23. Patrons de comportament: Cadena de responsabilitat i Iterador
  24. Patrons arquitectònics i d'empresa
  25. Antipatrons
  26. Taula final: problema → patró candidat
  27. Errors Comuns i Consells
  28. Exercicis
  29. Conclusió

  1. Què és un patró i què no

Un patró de disseny té quatre elements:

Element Què és Exemple (Estratègia)
Nom Vocabulari compartit «Estratègia»
Problema Quan aplicar-lo Diverses variants d'un algorisme, triades en execució
Solució L'estructura, no el codi Una interfície amb l'algorisme, implementacions intercanviables, un context que delega
Conseqüències El que guanyes i el que pagues Guanyes extensibilitat; pagues una classe per variant

El que un patró no és:

  • No és codi per copiar. És una estructura. La implementació depèn del llenguatge: a Java 8+, molts patrons GoF es redueixen a una lambda.
  • No és obligatori. Si if (tipus == LLIBRE) ... else ... cobreix dos casos que no creixeran, un if està bé.
  • No és una mesura de qualitat. Un projecte amb quinze patrons no és millor que un amb tres. Sol ser pitjor.
  • No és universal en el temps. Diversos patrons GoF existeixen per suplir mancances de C++ i Java de 1994. A Java 21, alguns són una línia.

  1. El catàleg GoF i el seu context històric

El 1994, Gamma, Helm, Johnson i Vlissides —la Gang of Four— van publicar Design Patterns, catalogant 23 patrons observats en sistemes reals de C++ i Smalltalk. S'organitzen en tres famílies:

Família S'ocupa de Patrons (els que es fan servir avui en negreta)
Creacionals Com es creen els objectes Singleton, Factory Method, Abstract Factory, Builder, Prototype
Estructurals Com es componen Adaptador, Decorador, Proxy, Facade, Composite, Bridge, Flyweight
De comportament Com col·laboren i reparteixen responsabilitats Estratègia, Observador, Template Method, Estat, Ordre, Iterador, Cadena de responsabilitat, Mediator, Memento, Visitor, Interpreter

Dos avisos històrics importants:

  1. Java en té molts d'incorporats. Iterator és al llenguatge (for-each). Observador és l'API d'esdeveniments de Spring. Estratègia és qualsevol interfície funcional. Els autors del llibre van dir explícitament que un patró és una mancança del llenguatge: quan el llenguatge millora, el patró desapareix.
  2. Alguns han envellit malament. El Singleton clàssic avui es considera majoritàriament un antipatró (ho veurem). Observer/Observable de java.util estan obsolets des de Java 9.

  1. Principis abans que patrons

Els patrons són solucions concretes; els principis són els criteris que et diuen si una solució és bona. Si haguessis de triar entre memoritzar els 23 patrons o interioritzar SOLID, tria SOLID sense dubtar: amb els principis dedueixes els patrons, al revés no.

flowchart TD
    P["PRINCIPIS<br/>SOLID, DRY, KISS, YAGNI, Demeter"]
    PA["PATRONS<br/>Estrategia, Decorador, Adaptador…"]
    C["CODI<br/>BiblioTech"]
    P -->|"justifiquen"| PA
    PA -->|"estructuren"| C
    C -->|"revela problemes que exigeixen"| P

  1. SOLID: responsabilitat única (S)

Una classe ha de tenir una sola raó per canviar.

L'enunciat popular «una classe, una cosa» és enganyós, perquè «una cosa» no està definit. La formulació útil és la de les raons per canviar: si dues persones diferents, amb motius diferents, et poden demanar que toquis la mateixa classe, aquesta classe té dues responsabilitats.

Abans (el GestorPrestecs que BiblioTech va arribar a tenir):

@Service
public class GestorPrestecs {

    public void prestar(String isbn, Long idEmpleat, int dies) {
        // 1. Validacio d'entrada
        if (isbn == null || !isbn.matches("97[89]-\\d{10}")) {
            throw new IllegalArgumentException("ISBN invalid");
        }
        // 2. Acces a dades
        Material m = em.createQuery("select m from Material m where m.isbn = :i", Material.class)
                       .setParameter("i", isbn).getSingleResult();
        // 3. Regles de negoci
        long actius = em.createQuery("select count(p) from Prestec p where …").getSingleResult();
        if (actius >= 3) throw new LimitExceditException();
        // 4. Persistencia
        Prestec p = new Prestec(m, empleat, LocalDate.now(), LocalDate.now().plusDays(dies));
        em.persist(p);
        // 5. Notificacio
        smtp.enviar(empleat.getCorreu(), "Prestec confirmat", "Has agafat " + m.getTitol());
        // 6. Format de sortida
        System.out.printf("%-40s %s%n", m.getTitol(), p.getDataVenciment());
        // 7. Auditoria
        Files.writeString(Path.of("auditoria.log"), LocalDateTime.now() + " PRESTEC " + isbn + "\n",
                          StandardOpenOption.APPEND);
    }
}

Set raons per canviar: el format de l'ISBN, el model de dades, la regla del límit, el mapatge, el proveïdor de correu, el format de la consola i el d'auditoria. Cadascuna la demana una persona diferent.

Després:

// DOMINI: la regla, i nomes la regla
public class PoliticaPrestecs {
    private final int maximActius;

    public void verificarPotPrestar(Empleat empleat, List<Prestec> actius) {
        if (actius.size() >= maximActius) {
            throw new LimitPrestecsExceditException(empleat.getNom(), maximActius);
        }
        if (empleat.teMultesPendents()) {
            throw new MultesPendentsException(empleat.getNom());
        }
    }
}

// APLICACIO: orquestracio, i nomes orquestracio
public class GestorPrestecs implements GestionarPrestecs {

    private final RepositoriMaterials materials;
    private final RepositoriPrestecs prestecs;
    private final RepositoriEmpleats empleats;
    private final PoliticaPrestecs politica;
    private final NotificadorAvisos notificador;
    private final Clock rellotge;

    @Override
    public Prestec prestar(Isbn isbn, Long idEmpleat, int dies) {
        Material material = materials.cercarPerIsbn(isbn)
                .orElseThrow(() -> new MaterialNoTrobatException(isbn));
        Empleat empleat = empleats.cercarPerId(idEmpleat)
                .orElseThrow(() -> new EmpleatNoTrobatException(idEmpleat));

        politica.verificarPotPrestar(empleat, prestecs.actiusDe(empleat));

        Prestec prestec = Prestec.nou(material, empleat, LocalDate.now(rellotge), dies);
        prestecs.guardar(prestec);
        notificador.notificar(Avis.perPrestec(prestec));
        return prestec;
    }
}

La validació de l'ISBN viu a l'objecte de valor Isbn (impossible construir-ne un d'invàlid). Les consultes viuen als repositoris. El correu, a l'adaptador. El format de sortida, a la CLI o al DTO. L'auditoria, en un aspecte (11-02) o en un @EntityListener.

Compte amb l'extrem contrari. Una classe per mètode és tan dolent com una classe per sistema: reparteixes la lògica en vint fitxers i cal llegir-los tots per entendre un cas d'ús. El criteri és raons per canviar, no nombre de línies.

  1. SOLID: obert/tancat (O)

Una entitat ha d'estar oberta a l'extensió i tancada a la modificació: afegir comportament nou sense tocar el codi existent.

És la justificació teòrica del polimorfisme que vas estudiar a 03-06.

Abans — cada tipus nou de material obliga a tocar el mateix switch, i a recordar tocar els altres quatre repartits pel projecte:

public int diesDePrestec(Material material) {
    switch (material.getTipus()) {
        case LLIBRE:  return 15;
        case REVISTA: return 7;
        case DVD:     return 3;
        default: throw new IllegalStateException("Tipus desconegut: " + material.getTipus());
    }
}

Després — cada tipus sap la seva pròpia política, i afegir AudioLlibre és crear una classe:

public abstract class Material {
    // Cada subtipus respon per si mateix; la resta del sistema no canvia.
    public abstract int diesPrestecPerDefecte();
    public abstract Diner multaPerDia();
}

public class Llibre extends Material {
    @Override public int diesPrestecPerDefecte() { return 15; }
    @Override public Diner multaPerDia() { return Diner.euros("0.50"); }
}

public class Revista extends Material {
    @Override public int diesPrestecPerDefecte() { return 7; }
    @Override public Diner multaPerDia() { return Diner.euros("0.25"); }
}

public class Dvd extends Material {
    @Override public int diesPrestecPerDefecte() { return 3; }
    @Override public Diner multaPerDia() { return Diner.euros("1.00"); }
}

Quan NO aplicar-lo. No abstreguis per si de cas. La versió amb switch és perfectament raonable si:

  • Els casos són tancats i no creixeran (els dies de la setmana, per exemple).
  • El switch és en un sol lloc.
  • Amb sealed (10-06), el compilador t'obliga a cobrir tots els casos, cosa que elimina el risc principal.

El problema del switch no és el switch: és tenir el mateix switch en set llocs i oblidar-te'n d'un.

  1. SOLID: substitució de Liskov (L)

Si S és subtipus de T, s'ha de poder substituir T per S sense que el programa deixi de funcionar correctament.

És el principi que més es viola sense adonar-se'n, perquè el compilador no ajuda: la violació és semàntica.

Exemple de violació a BiblioTech. Els materials de la secció de referència no es presten, només es consulten a la sala. Temptació:

public class MaterialDeReferencia extends Llibre {

    @Override
    public Prestec prestarA(Empleat empleat, int dies) {
        throw new UnsupportedOperationException("Els materials de referencia no es presten");
    }
}

Compila perfectament. I trenca tot el codi que treballa amb Material:

// Aquest metode era correcte. Ara peta en produccio quan en Diego Alonso
// afegeix un material de referencia al carret de prestec.
public void prestarTots(List<Material> materials, Empleat empleat) {
    for (Material m : materials) {
        m.prestarA(empleat, m.diesPrestecPerDefecte());   // UnsupportedOperationException
    }
}

Els tres símptomes clàssics de violació de Liskov:

Símptoma Exemple
El subtipus llança excepció on el supertipus no ho feia MaterialDeReferencia.prestarA
El subtipus enforteix les precondicions El pare accepta 1-30 dies; el fill només 1-7
El subtipus afebleix les postcondicions El pare garanteix llista no nul·la; el fill retorna null
El codi client necessita instanceof per funcionar if (m instanceof MaterialDeReferencia) continue;

Solució correcta: no forçar l'herència. Separar la capacitat de la jerarquia, que és exactament la segregació d'interfícies:

/** No tot material es prestable. La capacitat es modela a part del tipus. */
public interface Prestable {
    Prestec prestarA(Empleat empleat, int dies);
    int diesPrestecPerDefecte();
}

public abstract class Material {                 // tots: titol, ISBN, ubicacio
    public abstract String getTitol();
    public abstract Isbn getIsbn();
}

public class Llibre extends Material implements Prestable { … }
public class MaterialDeReferencia extends Material { }   // simplement NO es Prestable

I ara la signatura diu la veritat, i el compilador la fa complir:

public void prestarTots(List<Prestable> materials, Empleat empleat) { … }
// Ja es IMPOSSIBLE passar-li un MaterialDeReferencia.

Regla pràctica: si en escriure una subclasse sents la necessitat de llançar UnsupportedOperationException o de buidar un mètode heretat, la relació no és «és un». Reconsidera la jerarquia.

  1. SOLID: segregació d'interfícies (I)

Cap client no ha de veure's obligat a dependre de mètodes que no fa servir.

A 04-01 es van crear Prestable i Notificable com a interfícies separades, i ara té nom.

Abans — la interfície que ho sap tot:

public interface GestioBiblioteca {
    Prestec prestar(Isbn isbn, Long idEmpleat, int dies);
    void retornar(Long idPrestec);
    void reservar(Isbn isbn, Long idEmpleat);
    void enviarAvis(Avis avis);
    InformeMensual generarInforme(YearMonth periode);
    void importarCataleg(Path fitxer);
    void exportarCataleg(Path desti);
}

Conseqüències: qualsevol implementació ha d'implementar els set mètodes, encara que només n'hi interessin dos; a les proves, un doble manual necessita set mètodes buits; i canviar la signatura de generarInforme recompila tothom que només feia servir prestar.

Després — interfícies petites, orientades al client:

public interface Prestable {
    Prestec prestarA(Empleat empleat, int dies);
}

public interface Notificable {
    String destiDeNotificacio();
}

public interface GestionarPrestecs {            // port d'entrada del cas d'us
    Prestec prestar(Isbn isbn, Long idEmpleat, int dies);
    void retornar(Long idPrestec);
}

public interface ConsultarCataleg {
    List<Material> cercar(CriteriCerca criteri);
    Optional<Material> perIsbn(Isbn isbn);
}

Regla mnemotècnica: les interfícies es dissenyen des del client, no des de la implementació. La pregunta correcta no és «què sap fer aquesta classe?», sinó «què necessita qui la crida?».

Compte amb l'extrem: una interfície per mètode produeix un eixam de tipus. Si dos mètodes es fan servir sempre junts, van junts.

  1. SOLID: inversió de dependències (D)

Els mòduls d'alt nivell no han de dependre dels de baix nivell. Tots dos han de dependre d'abstraccions. Les abstraccions no han de dependre dels detalls; els detalls han de dependre de les abstraccions.

És el que estructura BiblioTech des de 12-01, i el que Spring automatitza des d'11-02.

Abans:

public class GestorPrestecs {
    // La politica d'alt nivell depen d'un detall de baix nivell.
    private final PrestecRepositoryJpa repositori = new PrestecRepositoryJpa();
    private final ClientMetadadesHttp metadades = new ClientMetadadesHttp();
}

Per provar el càlcul d'un préstec necessites base de dades i xarxa. Canviar a MongoDB és reescriure la classe.

Després:

public class GestorPrestecs {
    private final RepositoriPrestecs repositori;   // abstraccio definida al domini
    private final PassarelaMetadades metadades;

    public GestorPrestecs(RepositoriPrestecs repositori, PassarelaMetadades metadades) {
        this.repositori = repositori;
        this.metadades = metadades;
    }
}

Un matís que gairebé ningú no explica i que convé tenir clar:

Concepte Què és
Inversió de dependències (DIP) El principi: dependre d'abstraccions que defineix el mòdul d'alt nivell
Inversió de control (IoC) El patró general: algú extern controla el flux (el framework crida el teu codi)
Injecció de dependències (DI) La tècnica concreta: les dependències arriben des de fora, per constructor

Spring et dona IoC i DI. El DIP no te'l dona Spring: depèn que la interfície la defineixis tu al domini i no a la infraestructura. Si RepositoriPrestecs viu al paquet de persistència i estén JpaRepository, estàs fent servir DI sense DIP.

  1. DRY, KISS, YAGNI i la llei de Demeter

Principi Enunciat El matís que importa
DRY (Don't Repeat Yourself) Cada peça de coneixement ha de tenir una representació única És sobre coneixement, no sobre text. Dos mètodes idèntics per casualitat no són duplicació: acoblar-los crea un problema
KISS (Keep It Simple) Prefereix la solució més simple que funcioni Simple no és «poc codi»: és «fàcil d'entendre»
YAGNI (You Aren't Gonna Need It) No implementis el que no necessites avui El cost no és escriure-ho: és mantenir-ho, provar-ho i que confongui qui ho llegeixi
Demeter Parla només amb els teus amics immediats Cadenes llargues de getters revelen que la lògica és al lloc equivocat

La llei de Demeter, que reprèn 03-07. Un mètode d'A només hauria de cridar: mètodes del mateix A, dels seus paràmetres, d'objectes que ell mateix crea, i dels seus camps directes.

// VIOLACIO: quatre nivells de profunditat. Coneixes tota l'estructura interna.
String ciutat = prestec.getEmpleat().getDepartament().getSeu().getAdreca().getCiutat();

El problema és concret: aquesta línia es trenca si canvia qualsevol dels quatre models, i falla amb NullPointerException en qualsevol dels quatre punts.

// CORRECTE: cada objecte exposa el que li pregunten, i amaga com ho sap.
String ciutat = prestec.ciutatDeLempleat();

// a Prestec
public String ciutatDeLempleat() { return empleat.ciutat(); }
// a Empleat
public String ciutat() { return departament.ciutat(); }

Excepció explícita: les API fluides (Builder, Stream, AssertJ) encadenen expressament, i no violen Demeter, perquè cada crida retorna el mateix objecte o un del mateix tipus, no navega per una estructura aliena.

  1. Patrons creacionals: Singleton

Problema: garantir que existeix una única instància d'una classe i donar un punt d'accés global.

Solució clàssica:

public class ConfiguracioBiblioTech {

    private static final ConfiguracioBiblioTech INSTANCIA = new ConfiguracioBiblioTech();

    private ConfiguracioBiblioTech() { }           // ningu mes no la pot construir

    public static ConfiguracioBiblioTech getInstancia() { return INSTANCIA; }
}

I la variant idiomàtica en Java, amb enum, que a més és immune a la serialització i a la reflexió:

public enum ConfiguracioBiblioTech {
    INSTANCIA;

    private final int diesPerDefecte = 15;
    public int diesPerDefecte() { return diesPerDefecte; }
}
classDiagram
    class Singleton {
        -static INSTANCIA: Singleton
        -Singleton()
        +static getInstancia() Singleton
    }
    class Client
    Client ..> Singleton : getInstancia()

I ara la part important: per què el bean de Spring ho fa millor.

Aspecte Singleton clàssic Bean singleton de Spring
Unicitat Per ClassLoader de la JVM Per contenidor
Com l'obté el client getInstancia() dins del codi Injectat per constructor
Dependència visible No: oculta al cos del mètode Sí: apareix a la signatura del constructor
Substituïble en proves No, sense trucs bruts Sí: hi passes un altre objecte
Configurable per entorn No Sí: perfils, propietats
Cicle de vida Fix @PostConstruct, @PreDestroy, àmbits
Estat mutable compartit Perill de concurrència ocult El mateix perill, però explícit

El problema de fons del Singleton clàssic és que és una variable global disfressada. ConfiguracioBiblioTech.getInstancia() escrit dins de CalculadoraMultes crea una dependència que no apareix en cap signatura, que ningú no pot substituir i que fa la classe impossible de provar amb una altra configuració.

Regla: en una aplicació amb contenidor, no escriguis Singletons. Declara un bean. Si no hi ha contenidor, fes servir enum i limita l'estat al que és immutable.

  1. Patrons creacionals: Factory Method i Abstract Factory

Factory Method — problema: crear objectes sense acoblar el client a la classe concreta, i sense que un constructor ho hagi de decidir tot.

A BiblioTech, la importació del catàleg rep registres amb un camp tipus:

public final class FabricaMaterials {

    private FabricaMaterials() { }

    public static Material desDe(RegistreImportacio registre) {
        return switch (registre.tipus()) {
            case LLIBRE  -> new Llibre(registre.titol(), new Isbn(registre.isbn()),
                                       registre.autor(), registre.pagines());
            case REVISTA -> new Revista(registre.titol(), new Isbn(registre.isbn()),
                                        registre.numero(), registre.periodicitat());
            case DVD     -> new Dvd(registre.titol(), new Isbn(registre.isbn()),
                                    registre.duracioMinuts());
        };
    }
}

Una variant molt útil en Java modern és el mètode de fàbrica estàtic a la mateixa classe, que aporta una cosa que el constructor no pot: nom.

public class Prestec {

    private Prestec(...) { }   // constructor privat: la creacio passa per les fabriques

    /** Prestec estandard, amb els dies per defecte del material. */
    public static Prestec nou(Material material, Empleat empleat, LocalDate avui) {
        return new Prestec(material, empleat, avui,
                           avui.plusDays(material.diesPrestecPerDefecte()),
                           EstatPrestec.ACTIU);
    }

    /** Prestec prorrogat per direccio: fins a 90 dies. */
    public static Prestec especial(Material material, Empleat empleat, LocalDate avui, int dies) {
        if (dies > 90) throw new IllegalArgumentException("Maxim 90 dies");
        return new Prestec(material, empleat, avui, avui.plusDays(dies), EstatPrestec.ACTIU);
    }
}

Avantatges sobre el constructor: té nom, pot retornar un subtipus, pot retornar una instància desada a la memòria cau i pot fallar abans de construir res.

Abstract Factory — problema: crear famílies d'objectes relacionats sense acoblar-se a les seves classes concretes.

A BiblioTech apareix en exportar informes en diversos formats, on cada format necessita un generador, un formatador de capçaleres i una extensió coherents entre si:

classDiagram
    class FabricaExportacio {
        <<interface>>
        +generador() GeneradorInforme
        +formatadorTaula() FormatadorTaula
        +extensio() String
    }
    class FabricaPdf
    class FabricaCsv
    class FabricaJson
    FabricaExportacio <|.. FabricaPdf
    FabricaExportacio <|.. FabricaCsv
    FabricaExportacio <|.. FabricaJson
public interface FabricaExportacio {
    GeneradorInforme generador();
    FormatadorTaula formatadorTaula();
    String extensio();

    static FabricaExportacio per(Format format) {
        return switch (format) {
            case PDF  -> new FabricaPdf();
            case CSV  -> new FabricaCsv();
            case JSON -> new FabricaJson();
        };
    }
}

Quan NO fer-los servir. Si només hi ha una implementació i no se'n preveuen més, la fàbrica és una capa d'indirecció sense cap avantatge. new Llibre(...) està perfectament.

  1. Patrons creacionals: Builder

Deute des de 03-04, i ara es paga.

Problema: un objecte amb molts paràmetres, uns quants opcionals. El constructor telescòpic és il·legible i perillós:

// Que significa el 15? I el true? I el segon null?
Prestec p = new Prestec(material, empleat, LocalDate.now(), 15, true, null, false, null, "Marta Ruiz");

El perill real: si dos paràmetres consecutius són del mateix tipus, intercanviar-los compila i produeix un error silenciós en producció.

Solució:

public class Prestec {

    private final Material material;
    private final Empleat empleat;
    private final LocalDate dataPrestec;
    private final LocalDate dataVenciment;
    private final boolean renovable;
    private final String observacions;
    private final Empleat autoritzatPer;
    private final EstatPrestec estat;

    private Prestec(Builder b) {
        this.material = b.material;
        this.empleat = b.empleat;
        this.dataPrestec = b.dataPrestec;
        this.dataVenciment = b.dataVenciment;
        this.renovable = b.renovable;
        this.observacions = b.observacions;
        this.autoritzatPer = b.autoritzatPer;
        this.estat = EstatPrestec.ACTIU;
    }

    public static Builder de(Material material, Empleat empleat) {
        return new Builder(material, empleat);       // obligatoris a la fabrica del builder
    }

    public static class Builder {
        // Obligatoris: final, exigits al constructor del builder
        private final Material material;
        private final Empleat empleat;
        // Opcionals: amb valor per defecte sensat
        private LocalDate dataPrestec = LocalDate.now();
        private LocalDate dataVenciment;
        private boolean renovable = true;
        private String observacions = "";
        private Empleat autoritzatPer;

        private Builder(Material material, Empleat empleat) {
            this.material = Objects.requireNonNull(material, "material");
            this.empleat = Objects.requireNonNull(empleat, "empleat");
        }

        public Builder desDel(LocalDate data)      { this.dataPrestec = data; return this; }
        public Builder durant(int dies)            { this.dataVenciment = dataPrestec.plusDays(dies); return this; }
        public Builder finsA(LocalDate data)       { this.dataVenciment = data; return this; }
        public Builder noRenovable()               { this.renovable = false; return this; }
        public Builder ambObservacions(String o)   { this.observacions = o; return this; }
        public Builder autoritzatPer(Empleat e)    { this.autoritzatPer = e; return this; }

        public Prestec construir() {
            // La validacio de coherencia es fa UNA VEGADA, aqui:
            // aixi l'objecte construit es sempre valid.
            if (dataVenciment == null) {
                dataVenciment = dataPrestec.plusDays(material.diesPrestecPerDefecte());
            }
            if (dataVenciment.isBefore(dataPrestec)) {
                throw new IllegalStateException("El venciment no pot ser anterior al prestec");
            }
            if (!material.esPrestable() && autoritzatPer == null) {
                throw new IllegalStateException("Un material restringit requereix autoritzacio");
            }
            return new Prestec(this);
        }
    }
}

Ús, que ara es llegeix com una frase:

Prestec p = Prestec.de(javaEficac, martaRuiz)
        .desDel(LocalDate.of(2026, 3, 1))
        .durant(30)
        .noRenovable()
        .ambObservacions("Per al curs intern de Java")
        .autoritzatPer(nuriaVidal)
        .construir();
classDiagram
    class Prestec {
        -Prestec(Builder)
        +static de(Material, Empleat) Builder
    }
    class Builder {
        -material
        -empleat
        -dataVenciment
        +desDel(LocalDate) Builder
        +durant(int) Builder
        +noRenovable() Builder
        +construir() Prestec
    }
    Prestec +.. Builder : classe interna estatica
    Builder ..> Prestec : construeix

Contrast amb les alternatives de Java modern:

Alternativa Avantatges Inconvenients Quan
Constructor Directe, sense codi extra Il·legible amb més de 4 paràmetres; perillós amb tipus repetits Pocs paràmetres, tots obligatoris
record Una línia; immutable; equals/hashCode/toString de franc Sense opcionals; sense validació per passos; tots els camps al constructor canònic DTO i objectes de valor
Builder a mà Llegible; validació en un punt; opcionals naturals ~40 línies per classe; duplica els camps Objectes complexos, amb regles de coherència
@Builder de Lombok Zero codi repetitiu Màgia; genera setters implícits al builder; perillós en entitats JPA (11-07) Quan l'equip ja fa servir Lombok amb criteri

Combinació molt útil: record + builder per a un objecte de valor amb molts camps opcionals.

public record CriteriCerca(String titol, String autor, TipusMaterial tipus,
                           Integer anyInici, Integer anyFi, Boolean nomesDisponibles) {

    public static Builder builder() { return new Builder(); }

    public static class Builder {
        private String titol; private String autor; private TipusMaterial tipus;
        private Integer anyInici; private Integer anyFi;
        private Boolean nomesDisponibles = Boolean.FALSE;

        public Builder titol(String t) { this.titol = t; return this; }
        public Builder autor(String a) { this.autor = a; return this; }
        public Builder tipus(TipusMaterial t) { this.tipus = t; return this; }
        public Builder entre(int inici, int fi) { this.anyInici = inici; this.anyFi = fi; return this; }
        public Builder nomesDisponibles() { this.nomesDisponibles = true; return this; }

        public CriteriCerca construir() {
            return new CriteriCerca(titol, autor, tipus, anyInici, anyFi, nomesDisponibles);
        }
    }
}

Quan NO fer servir Builder: amb tres paràmetres obligatoris i cap d'opcional, el builder és quaranta línies per no guanyar res. I mai en entitats JPA: Hibernate necessita un constructor sense arguments i setters per als camps gestionats.

  1. Patrons creacionals: Prototype

Problema: crear un objecte nou copiant-ne un d'existent, quan construir-lo des de zero és car o la seva configuració és complexa.

A BiblioTech apareix de manera marginal: duplicar un CriteriCerca desat per la Núria Vidal per modificar-ne un camp. En Java modern això es resol millor amb mètodes «wither» sobre un record:

public record CriteriCerca(String titol, String autor, TipusMaterial tipus, …) {

    public CriteriCerca ambTipus(TipusMaterial nou) {
        return new CriteriCerca(titol, autor, nou, anyInici, anyFi, nomesDisponibles);
    }
}

El Cloneable de Java és la implementació clàssica del patró, i està pràcticament desaconsellada: clone() és superficial, no crida constructors, interactua malament amb final i el seu contracte està mal especificat. No el facis servir. Copia amb un constructor de còpia o amb mètodes com l'anterior.

  1. Patrons estructurals: Adaptador

Problema: una classe existent té la funcionalitat que necessites, però amb una interfície incompatible amb la que el teu sistema espera.

És exactament la situació de ClientMetadades (mòdul 9, reescrit a 11-07): parla HTTP i JSON, retorna DTO d'una API externa, llança IOException i InterruptedException, i el domini no n'ha de saber res.

classDiagram
    class PassarelaMetadades {
        <<interface>>
        +cercar(Isbn) Optional~MetadadesMaterial~
    }
    class AdaptadorMetadadesHttp {
        -ClientMetadades client
        +cercar(Isbn) Optional~MetadadesMaterial~
    }
    class ClientMetadades {
        +consultarPerIsbn(String) RespostaApiDto
    }
    PassarelaMetadades <|.. AdaptadorMetadadesHttp
    AdaptadorMetadadesHttp --> ClientMetadades : delega

El port, al domini, expressat en el llenguatge del domini:

package com.nexussoftware.bibliotech.domini.cataleg.port;

public interface PassarelaMetadades {
    /** Buit si no hi ha metadades o si el servei no esta disponible. */
    Optional<MetadadesMaterial> cercar(Isbn isbn);
}

L'adaptador, a infraestructura:

package com.nexussoftware.bibliotech.infraestructura.metadades;

@Component
class AdaptadorMetadadesHttp implements PassarelaMetadades {

    private static final Logger log = LoggerFactory.getLogger(AdaptadorMetadadesHttp.class);

    private final ClientMetadades client;     // l'"adaptee": la classe amb interficie incompatible

    AdaptadorMetadadesHttp(ClientMetadades client) { this.client = client; }

    @Override
    public Optional<MetadadesMaterial> cercar(Isbn isbn) {
        try {
            RespostaApiDto dto = client.consultarPerIsbn(isbn.valor());     // format extern

            // 1. Tradueix el model extern al del domini
            // 2. Tradueix les excepcions (frontera d'errors, 06-07)
            // 3. Aïlla el domini del fet que existeixin HTTP, JSON i aquesta API concreta
            return Optional.of(new MetadadesMaterial(
                    dto.title(),
                    dto.authors() == null ? List.of() : dto.authors(),
                    dto.publishedDate() == null ? null : Year.parse(dto.publishedDate().substring(0, 4)),
                    dto.pageCount()));

        } catch (RecursNoTrobatException e) {
            return Optional.empty();                     // "no hi ha dades" no es un error
        } catch (IOException | InterruptedException e) {
            if (e instanceof InterruptedException) Thread.currentThread().interrupt();
            log.warn("Metadades no disponibles per a {}: {}", isbn, e.getMessage());
            return Optional.empty();                     // degradacio elegant
        }
    }
}

Fixa't en les tres traduccions que fa un adaptador ben fet: de tipus, d'excepcions i de semàntica (un 404 extern es converteix en Optional.empty(), no en un error).

Quan NO fer-lo servir: si controles la classe d'origen i en pots canviar la interfície, canvia-la. L'adaptador és per al que no pots tocar: llibreries de tercers, API externes, codi heretat.

  1. Patrons estructurals: Decorador

Deute des de 07-03, i aquí es paga.

Problema: afegir responsabilitats a un objecte dinàmicament, sense modificar-ne la classe ni crear una subclasse per cada combinació.

El cas canònic: els fluxos d'E/S de Java. El mòdul 7 el feia servir sense anomenar-lo:

// Cada embolcall AFEGEIX una capacitat a l'anterior, i tots son InputStream.
InputStream base       = Files.newInputStream(Path.of("cataleg.csv"));   // bytes crus
InputStream intermedi  = new BufferedInputStream(base);                  // + memoria intermedia
InputStream descomp    = new GZIPInputStream(intermedi);                 // + descompressio
Reader      lector     = new InputStreamReader(descomp, UTF_8);          // + codificacio
BufferedReader linies  = new BufferedReader(lector);                     // + readLine()

Sense el patró caldrien classes com BufferedGzipUtf8InputStream: una per combinació. Amb ell, quatre classes donen setze combinacions.

El cas de BiblioTech: desar el catàleg a la memòria cau sense tocar el repositori.

classDiagram
    class RepositoriMaterials {
        <<interface>>
        +cercarPerIsbn(Isbn) Optional~Material~
        +cercar(CriteriCerca) List~Material~
    }
    class RepositoriMaterialsJpa
    class CatalegAmbCache {
        -RepositoriMaterials delegat
        -Map cache
    }
    class CatalegAmbMetriques {
        -RepositoriMaterials delegat
    }
    RepositoriMaterials <|.. RepositoriMaterialsJpa
    RepositoriMaterials <|.. CatalegAmbCache
    RepositoriMaterials <|.. CatalegAmbMetriques
    CatalegAmbCache --> RepositoriMaterials : embolcalla
    CatalegAmbMetriques --> RepositoriMaterials : embolcalla
/**
 * Decorador: ES un RepositoriMaterials i TE un RepositoriMaterials.
 * Aquesta doble relacio es la signatura del patro.
 */
public class CatalegAmbCache implements RepositoriMaterials {

    private static final Logger log = LoggerFactory.getLogger(CatalegAmbCache.class);

    private final RepositoriMaterials delegat;
    private final Map<Isbn, Material> cache = new ConcurrentHashMap<>();   // modul 8

    public CatalegAmbCache(RepositoriMaterials delegat) {
        this.delegat = delegat;
    }

    @Override
    public Optional<Material> cercarPerIsbn(Isbn isbn) {
        Material enCache = cache.get(isbn);
        if (enCache != null) {
            log.debug("Encert de memoria cau per a {}", isbn);
            return Optional.of(enCache);
        }
        Optional<Material> resultat = delegat.cercarPerIsbn(isbn);
        resultat.ifPresent(m -> cache.put(isbn, m));
        return resultat;
    }

    @Override
    public List<Material> cercar(CriteriCerca criteri) {
        return delegat.cercar(criteri);        // les cerques no es desen: son variables
    }

    @Override
    public Material guardar(Material material) {
        cache.remove(material.getIsbn());      // invalidacio: el mes dificil d'una memoria cau
        return delegat.guardar(material);
    }
}

I decoradors apilables, com els fluxos:

@Bean
RepositoriMaterials repositoriMaterials(RepositoriMaterialsJpa jpa, MeterRegistry registre) {
    return new CatalegAmbMetriques(          // 3r: mesura
             new CatalegAmbCache(            // 2n: desa a la memoria cau
               new CatalegAmbReintents(jpa, 3)));     // 1r: reintenta
}

Ningú que faci servir RepositoriMaterials no sap que hi ha tres capes. I l'ordre importa: en aquest muntatge, les mètriques mesuren el temps amb memòria cau; si volguessis mesurar només la base de dades, el decorador de mètriques aniria a dins.

Patró Mateixa interfície que l'embolcallat Intenció
Decorador Afegir comportament
Adaptador No (aquesta és la gràcia) Canviar la interfície
Proxy Controlar l'accés

Decorador i Proxy són estructuralment idèntics: es distingeixen per la intenció.

Quan NO fer-lo servir: si només hi ha una decoració i mai no n'hi haurà més, un if a la implementació és més simple. I si necessites cinc capes, la depuració es torna dolorosa: la pila de crides s'omple d'embolcalls.

  1. Patrons estructurals: Proxy

Problema: controlar l'accés a un objecte — per retardar-ne la creació, verificar permisos, registrar crides, reintentar o gestionar una transacció.

Ja vas escriure proxies dinàmics a 10-03. Ara tenen nom i context.

// Proxy dinamic del modul 10-03: registra el temps de cada crida
@SuppressWarnings("unchecked")
public static <T> T ambCronometre(T objectiu, Class<T> interficie) {
    return (T) Proxy.newProxyInstance(
        interficie.getClassLoader(),
        new Class<?>[]{interficie},
        (proxy, metode, args) -> {
            long inici = System.nanoTime();
            try {
                return metode.invoke(objectiu, args);
            } finally {
                long ms = (System.nanoTime() - inici) / 1_000_000;
                LoggerFactory.getLogger(interficie).debug("{} ha trigat {} ms", metode.getName(), ms);
            }
        });
}

I aquí es tanca un cercle del mòdul 11. Quan escrius això:

@Service
public class GestorPrestecs {
    @Transactional
    public Prestec prestar(Isbn isbn, Long idEmpleat, int dies) { … }
}

Spring no injecta un GestorPrestecs. Injecta un proxy de GestorPrestecs que, en cada crida a prestar, obre una transacció, crida el mètode real, i fa commit o rollback. És exactament el mecanisme de dalt.

sequenceDiagram
    participant C as PrestecController
    participant P as Proxy transaccional
    participant TM as PlatformTransactionManager
    participant S as GestorPrestecs real

    C->>P: prestar(isbn, id, 15)
    P->>TM: getTransaction()
    P->>S: prestar(isbn, id, 15)
    S-->>P: Prestec
    P->>TM: commit()
    P-->>C: Prestec
    Note over P,TM: si S llanca RuntimeException, el proxy fa rollback

Això explica d'una vegada les dues raresses més famoses de @Transactional:

  1. L'autoinvocació no funciona. Si metodeA() crida this.metodeB() i només metodeB és @Transactional, no hi ha transacció: la crida this. no passa pel proxy.
  2. Els mètodes private i final no s'intercepten. El proxy és una subclasse generada (CGLIB) o un implementador de la interfície (JDK): no pot sobreescriure el que no és visible ni sobreescrivible.

Tipus de proxy, tots presents a BiblioTech:

Tipus Per a què On és a BiblioTech
Virtual Retardar la creació d'una cosa cara Les relacions LAZY d'Hibernate (11-03)
De protecció Verificar permisos @PreAuthorize de Spring Security (12-07)
Remot Representar un objecte en una altra màquina Un client HTTP declaratiu
Intel·ligent Afegir lògica transversal @Transactional, @Cacheable, AspecteCronometre

  1. Patrons estructurals: Facade i Composite

Facade — problema: un subsistema té moltes classes i el client només necessita unes poques operacions. La façana ofereix una interfície simple sobre un conjunt complex.

Els serveis d'aplicació de BiblioTech són façanes: GestorPrestecs.prestar(...) amaga repositoris, política, rellotge i notificador. També ho era SLF4J a 11-07: una façana sobre implementacions de logging.

Avís: la façana degenera fàcilment en objecte Déu (ho veurem als antipatrons) si acumula operacions sense criteri. Una façana per àrea, no una per a tota l'aplicació.

Composite — problema: tractar de manera uniforme objectes individuals i composicions d'objectes.

A BiblioTech es veu a les col·leccions de materials: una «col·lecció temàtica» (per exemple, «Fonaments de disseny de programari») agrupa materials i altres col·leccions, i n'ha de poder consultar la disponibilitat com si fos un material més.

public interface ElementCataleg {
    String titol();
    int unitatsDisponibles();
}

public class Llibre implements ElementCataleg { … }               // fulla

public class ColleccioTematica implements ElementCataleg {        // compost
    private final String titol;
    private final List<ElementCataleg> elements;

    @Override
    public int unitatsDisponibles() {
        // Disponible si TOTS els seus elements ho estan: el minim mana
        return elements.stream().mapToInt(ElementCataleg::unitatsDisponibles).min().orElse(0);
    }
}

  1. Patrons de comportament: Estratègia

Problema: diverses variants d'un algorisme, seleccionables en temps d'execució, sense omplir el codi de condicionals.

Ja el tens des del mòdul 4: ReglaTarifa i FiltreMaterial eren estratègies.

classDiagram
    class ReglaTarifa {
        <<interface>>
        +calcular(int diesRetard, Material m) Diner
    }
    class TarifaEstandard
    class TarifaReduidaEmpleatNou
    class TarifaAgreujadaMaterialCritic
    class CalculadoraMultes {
        -ReglaTarifa regla
        +calcular(Prestec, LocalDate) Diner
    }
    ReglaTarifa <|.. TarifaEstandard
    ReglaTarifa <|.. TarifaReduidaEmpleatNou
    ReglaTarifa <|.. TarifaAgreujadaMaterialCritic
    CalculadoraMultes --> ReglaTarifa : delega

Abans:

public Diner calcularMulta(Prestec p, LocalDate avui) {
    int retard = (int) ChronoUnit.DAYS.between(p.getDataVenciment(), avui);
    if (retard <= 0) return Diner.ZERO;

    if (p.getEmpleat().antiguitatEnMesos() < 6) {
        return Diner.euros("0.25").per(retard);
    } else if (p.getMaterial().esCritic()) {
        return Diner.euros("2.00").per(retard);
    } else if (p.getEmpleat().esDireccio()) {
        return Diner.ZERO;
    } else {
        return Diner.euros("0.50").per(retard);
    }
}

Cada política nova és una branca més al mateix mètode, que a més cal tornar a provar sencer.

Després:

@FunctionalInterface
public interface ReglaTarifa {
    Diner calcular(int diesDeRetard, Prestec prestec);

    default boolean aplicaA(Prestec prestec) { return true; }
}

public class TarifaEstandard implements ReglaTarifa {
    @Override public Diner calcular(int dies, Prestec p) {
        return p.getMaterial().multaPerDia().per(dies).limitadaA(Diner.euros("20.00"));
    }
}

public class TarifaReduidaEmpleatNou implements ReglaTarifa {
    @Override public boolean aplicaA(Prestec p) { return p.getEmpleat().antiguitatEnMesos() < 6; }
    @Override public Diner calcular(int dies, Prestec p) { return Diner.euros("0.25").per(dies); }
}

public class TarifaExemptaDireccio implements ReglaTarifa {
    @Override public boolean aplicaA(Prestec p) { return p.getEmpleat().esDireccio(); }
    @Override public Diner calcular(int dies, Prestec p) { return Diner.ZERO; }
}

public class CalculadoraMultes {
    private final List<ReglaTarifa> regles;      // ordenades: la primera que aplica, guanya
    private final ReglaTarifa perDefecte = new TarifaEstandard();

    public CalculadoraMultes(List<ReglaTarifa> regles) { this.regles = List.copyOf(regles); }

    public Diner calcular(Prestec p, LocalDate avui) {
        int retard = (int) ChronoUnit.DAYS.between(p.getDataVenciment(), avui);
        if (retard <= 0) return Diner.ZERO;

        return regles.stream()
                .filter(r -> r.aplicaA(p))
                .findFirst()
                .orElse(perDefecte)
                .calcular(retard, p);
    }
}

I amb Spring, el patró es torna gairebé invisible. Si cada regla és un bean, Spring injecta la llista completa ordenada per @Order, i afegir una política és crear una classe: zero canvis a la resta del sistema.

@Component @Order(10) class TarifaExemptaDireccio implements ReglaTarifa { … }
@Component @Order(20) class TarifaReduidaEmpleatNou implements ReglaTarifa { … }

@Service
class CalculadoraMultes {
    private final List<ReglaTarifa> regles;
    CalculadoraMultes(List<ReglaTarifa> regles) { this.regles = regles; }   // Spring les injecta totes
}

A Java 8+, una estratègia sense estat és simplement una lambda o una referència a mètode: Comparator (mòdul 5), Predicate, Function són totes estratègies.

Quan NO fer-lo servir: amb dos casos estables (actiu/retornat), un if és més clar. El patró compensa quan les variants creixen o vénen de configuració.

  1. Patrons de comportament: Template Method

Deute des de 04-02.

Problema: diversos algorismes comparteixen la mateixa estructura, però difereixen en alguns passos.

A BiblioTech: importar el catàleg des de CSV, des de JSON o des de l'API de metadades. L'esquelet és idèntic —obrir, validar capçalera, llegir registres, convertir, desar, informar—; el que canvia són tres passos.

classDiagram
    class ImportadorCataleg {
        <<abstract>>
        +importar(Path) ResultatImportacio
        #obrir(Path)* Origen
        #llegirRegistres(Origen)* List~RegistreImportacio~
        #validar(RegistreImportacio) boolean
    }
    class ImportadorCsv
    class ImportadorJson
    class ImportadorApi
    ImportadorCataleg <|-- ImportadorCsv
    ImportadorCataleg <|-- ImportadorJson
    ImportadorCataleg <|-- ImportadorApi
public abstract class ImportadorCataleg {

    private static final Logger log = LoggerFactory.getLogger(ImportadorCataleg.class);

    private final RepositoriMaterials repositori;

    protected ImportadorCataleg(RepositoriMaterials repositori) {
        this.repositori = repositori;
    }

    /**
     * METODE PLANTILLA: defineix l'algorisme i es final perque ningu
     * no pugui alterar la sequencia. Nomes els passos son substituibles.
     */
    public final ResultatImportacio importar(Path origen) {
        log.info("Iniciant importacio des de {}", origen);
        long inici = System.nanoTime();

        int correctes = 0;
        List<ErrorImportacio> errors = new ArrayList<>();

        try (Origen o = obrir(origen)) {                        // PAS ABSTRACTE
            for (RegistreImportacio registre : llegirRegistres(o)) {   // PAS ABSTRACTE
                try {
                    if (!validar(registre)) {                   // GANXO amb implementacio per defecte
                        errors.add(ErrorImportacio.invalid(registre));
                        continue;
                    }
                    repositori.guardar(FabricaMaterials.desDe(registre));
                    correctes++;
                } catch (Exception e) {
                    errors.add(ErrorImportacio.de(registre, e));
                }
            }
        } catch (IOException e) {
            throw new ImportacioFallidaException(origen, e);
        }

        long ms = (System.nanoTime() - inici) / 1_000_000;
        enAcabar(correctes, errors);                            // GANXO opcional
        log.info("Importacio acabada: {} correctes, {} errors, {} ms",
                 correctes, errors.size(), ms);
        return new ResultatImportacio(correctes, errors, Duration.ofMillis(ms));
    }

    // --- Passos que cada subclasse HA d'aportar ---
    protected abstract Origen obrir(Path cami) throws IOException;
    protected abstract List<RegistreImportacio> llegirRegistres(Origen origen) throws IOException;

    // --- Ganxos: comportament per defecte, substituible ---
    protected boolean validar(RegistreImportacio r) {
        return r.isbn() != null && r.titol() != null && !r.titol().isBlank();
    }

    protected void enAcabar(int correctes, List<ErrorImportacio> errors) { }
}

Una subclasse queda mínima:

public class ImportadorCsv extends ImportadorCataleg {

    private final char separador;

    @Override
    protected Origen obrir(Path cami) throws IOException {
        return new OrigenLector(Files.newBufferedReader(cami, StandardCharsets.UTF_8));
    }

    @Override
    protected List<RegistreImportacio> llegirRegistres(Origen origen) throws IOException {
        try (CSVReader csv = new CSVReaderBuilder(origen.lector())
                .withSkipLines(1)
                .withCSVParser(new CSVParserBuilder().withSeparator(separador).build())
                .build()) {
            return csv.readAll().stream().map(this::aRegistre).toList();
        }
    }
}

Template Method enfront d'Estratègia — resolen problemes semblants amb mecanismes oposats:

Aspecte Template Method Estratègia
Mecanisme Herència Composició
Què es fixa L'algorisme complet, a la classe base Res: se substitueix sencer
Moment de l'elecció Compilació (quina subclasse instancies) Execució (quin objecte injectes)
Combinacions Una jerarquia per variació Es combinen lliurement
Risc Jerarquies profundes i fràgils Més objectes per coordinar

Preferència moderna: composició sobre herència. Fes servir Template Method quan l'esquelet sigui genuïnament fix i els passos, molts.

  1. Patrons de comportament: Observador

Problema: diversos objectes han de reaccionar a un fet, i qui el produeix no els ha de conèixer.

A BiblioTech, en retornar un material han de passar coses independents entre si: comprovar reserves pendents, actualitzar estadístiques, notificar l'empleat, registrar auditoria. Sense el patró, GestorPrestecs.retornar() acaba cridant cinc serveis i creixent amb cada requisit nou.

Versió clàssica:

public interface OientDevolucio {
    void enRetornar(EsdevenimentDevolucio esdeveniment);
}

public class GestorPrestecs {
    private final List<OientDevolucio> oients = new CopyOnWriteArrayList<>();     // modul 8

    public void registrar(OientDevolucio oient) { oients.add(oient); }

    public void retornar(Long id) {
        Prestec p = …;
        p.registrarDevolucio(LocalDate.now(rellotge));
        prestecs.guardar(p);

        EsdevenimentDevolucio esdeveniment = new EsdevenimentDevolucio(p.getId(), p.getIsbn(), …);
        for (OientDevolucio oient : oients) {
            try {
                oient.enRetornar(esdeveniment);
            } catch (Exception e) {
                // Un oient que falla NO pot impedir que s'executin els altres
                log.error("L'oient {} ha fallat", oient.getClass().getSimpleName(), e);
            }
        }
    }
}

Aquest try/catch dins del bucle és la part que gairebé tothom oblida i la que marca la diferència entre un patró útil i una fallada en cascada.

La versió moderna: esdeveniments d'aplicació de Spring. El mateix patró, sense escriure la infraestructura:

// L'esdeveniment: un record immutable
public record MaterialRetornat(Long idPrestec, Isbn isbn, Long idEmpleat,
                               LocalDate data, Diner multa) { }

// L'emissor: no coneix cap oient
@Service
public class GestorPrestecs {

    private final ApplicationEventPublisher esdeveniments;

    @Transactional
    public void retornar(Long id) {
        Prestec p = prestecs.cercarPerId(id).orElseThrow(…);
        Diner multa = p.registrarDevolucio(LocalDate.now(rellotge));
        prestecs.guardar(p);

        esdeveniments.publishEvent(new MaterialRetornat(p.getId(), p.getIsbn(),
                                                        p.getIdEmpleat(), LocalDate.now(rellotge), multa));
    }
}

// Els oients: independents, afegir-ne un no toca l'emissor
@Component
class ActivadorReserves {
    @EventListener
    void enRetornar(MaterialRetornat esdeveniment) {
        processadorReserves.activarSeguentReserva(esdeveniment.isbn());
    }
}

@Component
class ActualitzadorEstadistiques {
    // Nomes si la transaccio fa COMMIT: no volem comptar devolucions que es van desfer
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    void enRetornar(MaterialRetornat esdeveniment) {
        estadistiques.registrarDevolucio(esdeveniment);
    }
}

@Component
class NotificadorMultes {
    @Async                                     // en un altre fil: no bloqueja la resposta
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    void enRetornar(MaterialRetornat esdeveniment) {
        if (esdeveniment.multa().esPositiva()) {
            notificador.notificar(Avis.perMulta(esdeveniment));
        }
    }
}

@TransactionalEventListener(AFTER_COMMIT) mereix atenció: garanteix que la reacció només passa si les dades es van desar de debò. És la diferència entre notificar una multa real i notificar-ne una que es va desfer per un error posterior.

Quan NO fer-lo servir. Els esdeveniments fan el flux difícil de seguir: llegint retornar() no saps què passarà després. Si només hi ha un oient i sempre n'hi haurà un, crida'l directament. I java.util.Observer/Observable estan obsolets des de Java 9: no els facis servir.

  1. Patrons de comportament: Estat

Problema: un objecte canvia de comportament segons el seu estat intern, i el codi s'omple de condicionals sobre aquest estat.

BiblioTech té EstatPrestec des del mòdul 4. Amb el patró, cada estat sap quines transicions permet:

stateDiagram-v2
    [*] --> ACTIU : crear
    ACTIU --> RENOVAT : renovar
    RENOVAT --> RETORNAT : retornar
    ACTIU --> RETORNAT : retornar
    ACTIU --> VENCUT : passa la data
    RENOVAT --> VENCUT : passa la data
    VENCUT --> RETORNAT : retornar amb multa
    RETORNAT --> [*]
    VENCUT --> PERDUT : 90 dies sense retornar
    PERDUT --> [*]

En Java, l'enum amb mètodes abstractes és la implementació més neta del patró (04-07):

public enum EstatPrestec {

    ACTIU {
        @Override public boolean permetRenovar()  { return true; }
        @Override public boolean permetRetornar() { return true; }
        @Override public EstatPrestec enRenovar() { return RENOVAT; }
    },
    RENOVAT {
        @Override public boolean permetRenovar()  { return false; }    // nomes una renovacio
        @Override public boolean permetRetornar() { return true; }
    },
    VENCUT {
        @Override public boolean permetRenovar()  { return false; }
        @Override public boolean permetRetornar() { return true; }
        @Override public boolean generaMulta()    { return true; }
    },
    RETORNAT {
        @Override public boolean esFinal() { return true; }
    },
    PERDUT {
        @Override public boolean esFinal() { return true; }
        @Override public boolean generaMulta() { return true; }
    };

    public boolean permetRenovar()  { return false; }
    public boolean permetRetornar() { return false; }
    public boolean generaMulta()    { return false; }
    public boolean esFinal()        { return false; }

    public EstatPrestec enRenovar() {
        throw new TransicioInvalidaException(this, "renovar");
    }
}

I l'entitat es limita a preguntar, sense cap switch:

public void renovar(int dies) {
    if (!estat.permetRenovar()) {
        throw new TransicioInvalidaException(estat, "renovar");
    }
    this.dataVenciment = dataVenciment.plusDays(dies);
    this.estat = estat.enRenovar();
}

L'avantatge decisiu: la màquina d'estats és en un sol lloc. Sense el patró, la regla «només es pot renovar una vegada» apareix al servei, al controlador i a la CLI, i tard o d'hora una de les tres se n'oblida.

  1. Patrons de comportament: Ordre

Problema: encapsular una petició com un objecte, per poder parametritzar-la, encuar-la, registrar-la o desfer-la.

A 05-08 es va construir una pila de desfer amb Deque. Allò era el patró Ordre.

public interface Ordre {
    void executar();
    void desfer();
    String descripcio();
}

public class OrdrePrestar implements Ordre {

    private final GestionarPrestecs gestor;
    private final Isbn isbn;
    private final Long idEmpleat;
    private Long idPrestecCreat;        // estat necessari per desfer

    @Override
    public void executar() {
        this.idPrestecCreat = gestor.prestar(isbn, idEmpleat, 15).getId();
    }

    @Override
    public void desfer() {
        if (idPrestecCreat == null) throw new IllegalStateException("No executat");
        gestor.anullar(idPrestecCreat);
    }

    @Override
    public String descripcio() { return "Prestar " + isbn + " a l'empleat " + idEmpleat; }
}

public class HistorialOperacions {
    private final Deque<Ordre> pila = new ArrayDeque<>();        // 05-07/05-08

    public void executar(Ordre o) {
        o.executar();
        pila.push(o);
    }

    public void desferUltima() {
        if (pila.isEmpty()) throw new ResPerDesferException();
        pila.pop().desfer();
    }
}

Altres usos del mateix patró, tots presents a l'ecosistema: els Runnable i Callable que vas enviar a un ExecutorService al mòdul 8 són ordres, i les tasques encuades per a processament diferit també.

  1. Patrons de comportament: Cadena de responsabilitat i Iterador

Cadena de responsabilitat — problema: diversos gestors poden atendre una petició; es passa per la cadena fins que un la processa.

L'exemple canònic és al mateix Spring: la cadena de filtres de Spring Security (12-07) i els Filter de servlets. A BiblioTech, la validació d'una importació:

public interface ValidadorImportacio {
    Optional<ErrorImportacio> validar(RegistreImportacio r);
}

@Service
class CadenaValidacio {
    private final List<ValidadorImportacio> validadors;     // Spring els injecta tots, ordenats

    public Optional<ErrorImportacio> validar(RegistreImportacio r) {
        return validadors.stream()
                .map(v -> v.validar(r))
                .flatMap(Optional::stream)
                .findFirst();               // el primer error talla la cadena
    }
}

Iterador — problema: recórrer una col·lecció sense exposar-ne l'estructura interna.

Està incorporat al llenguatge des de Java 5: qualsevol Iterable funciona amb for-each (reprèn 05-02). Poques vegades cal implementar-lo, però quan cal —recórrer resultats paginats d'una API com si fossin una llista— és molt útil:

public class CatalegPaginat implements Iterable<Material> {

    private final RepositoriMaterials repositori;
    private final int midaPagina;

    @Override
    public Iterator<Material> iterator() {
        return new Iterator<>() {
            private int pagina = 0;
            private Iterator<Material> actual = Collections.emptyIterator();

            @Override public boolean hasNext() {
                if (actual.hasNext()) return true;
                List<Material> seguent = repositori.pagina(pagina++, midaPagina);
                actual = seguent.iterator();
                return actual.hasNext();
            }

            @Override public Material next() {
                if (!hasNext()) throw new NoSuchElementException();
                return actual.next();
            }
        };
    }
}
// El client recorre 50.000 materials sense carregar-los tots a memoria
for (Material m : new CatalegPaginat(repositori, 500)) { … }

  1. Patrons arquitectònics i d'empresa

A més dels GoF, hi ha patrons de nivell superior que BiblioTech ja fa servir, catalogats majoritàriament per Martin Fowler:

Patró Què resol On és a BiblioTech
Repositori Abstreure l'accés a dades com si fos una col·lecció en memòria RepositoriPrestecs i els repositoris de Spring Data (11-03)
Unitat de treball Agrupar canvis i aplicar-los en una sola transacció L'EntityManager de JPA amb @Transactional
DTO Transportar dades entre capes o processos Els record de 12-01 i 12-04
Servei d'aplicació Orquestrar casos d'ús sense lògica de domini GestorPrestecs, ProcessadorReserves
Injecció de dependències Subministrar col·laboradors des de fora Constructors + Spring (11-02)
Objecte de valor Modelar conceptes sense identitat Isbn, Diner
Especificació Compondre criteris de consulta CriteriCerca, Specification de Spring Data
Model de domini Posar la lògica en objectes amb estat i comportament Prestec.registrarDevolucio()

Sobre la Unitat de treball, convé que vegis el que JPA fa per sota, perquè explica per què no crides save() després de modificar una entitat:

@Transactional
public void renovar(Long id, int dies) {
    Prestec p = prestecs.cercarPerId(id).orElseThrow(…);
    p.renovar(dies);
    // NO cal prestecs.guardar(p):
    // el context de persistencia (la unitat de treball) rastreja l'entitat
    // i fa l'UPDATE en fer flush, abans del commit.
}

  1. Antipatrons

Objecte Déu. Una classe que ho sap i ho fa tot. Símptomes: més de 500 línies, més de 15 dependències, nom genèric (GestorGeneral, UtilBiblioTech, ServeiPrincipal), i tothom la modifica en cada sprint. És la violació pura de la responsabilitat única. Es cura extraient per raó de canvi, no per nombre de línies.

Singleton com a variable global. Ja vist: crea dependències ocultes, impedeix provar i amaga estat mutable compartit. La versió moderna del mateix pecat és la classe d'utilitats amb estat estàtic:

public final class ConfigGlobal {
    public static int diesPrestec = 15;     // qualsevol el canvia, des de qualsevol fil
}

Herència per conveniència. Heretar per reutilitzar codi, sense que hi hagi relació «és un»:

// MALAMENT: un gestor de prestecs NO ES una llista de prestecs.
public class GestorPrestecs extends ArrayList<Prestec> { … }

Conseqüència: qualsevol pot cridar clear() i esborrar tots els préstecs. La regla és composició sobre herència: si la relació no és «és un», fes servir un camp.

Model de domini anèmic — i aquí cal ser honestos, perquè afecta BiblioTech.

Un model anèmic és aquell en què les entitats són només dades amb getters i setters, i tota la lògica viu en serveis. Fowler el va anomenar antipatró perquè malbarata l'orientació a objectes: se separen dades i comportament, que és justament el que la POO ajunta.

// ANEMIC: Prestec es una bossa de dades
public class Prestec {
    private LocalDate dataVenciment;
    private EstatPrestec estat;
    // 15 getters i 15 setters, zero comportament
}

@Service
public class GestorPrestecs {
    public void retornar(Prestec p, LocalDate avui) {
        // La regla de negoci viu FORA de l'objecte que la protegeix
        if (p.getEstat() == EstatPrestec.RETORNAT) throw new …;
        p.setDataDevolucio(avui);
        p.setEstat(EstatPrestec.RETORNAT);
    }
}

El problema pràctic: res no impedeix que un altre servei cridi setEstat(RETORNAT) sense posar la data, i l'objecte queda en estat invàlid. Els invariants no es poden garantir si qualsevol pot mutar els camps.

// RIC: l'objecte protegeix les seves propies regles
public class Prestec {
    public Diner registrarDevolucio(LocalDate data) {
        if (estat.esFinal()) throw new PrestecJaTancatException(id);
        if (data.isBefore(dataPrestec)) throw new DataInvalidaException(data);
        this.dataDevolucio = data;
        this.estat = EstatPrestec.RETORNAT;
        return multaFinsA(data);
    }
    // sense setters publics: es IMPOSSIBLE deixar l'objecte a mitges
}

El debat honest. BiblioTech té lògica a les entitats (Prestec.registrarDevolucio(), Material.diesPrestecPerDefecte()), i això és deliberat. Però hi ha arguments legítims en contra:

A favor del model ric A favor del model anèmic
Els invariants es garanteixen en un sol lloc Les entitats JPA tenen cicle de vida gestionat: lògica complexa a dins complica el flush i les relacions mandroses
La lògica és on són les dades La lògica que necessita diversos agregats o serveis externs no cap en una entitat
Es prova sense infraestructura És el que la majoria d'equips coneix; es llegeix ràpid
Menys serveis anodins Separar dades de procés encaixa millor amb transaccions i amb estils funcionals

Postura defensable i la que aplica BiblioTech: entitats raonablement riques —els invariants propis de l'entitat viuen en ella—, i serveis d'aplicació per al que involucra diversos agregats, transaccions o infraestructura. Prestec sap que no es pot retornar dues vegades; GestorPrestecs sap que cal cercar el material, comprovar el límit de l'empleat i notificar. Cap de les dues coses no és al lloc equivocat.

El que no és defensable és una entitat amb 30 setters públics i tota la lògica en un servei de 800 línies.

  1. Taula final: problema → patró candidat

Problema que estàs tenint Patró candidat
Un constructor amb 6 paràmetres i la meitat opcionals Builder
Un switch sobre un tipus repetit en diversos llocs Polimorfisme (obert/tancat), Estratègia
Necessito diverses polítiques de càlcul intercanviables Estratègia
Una llibreria externa té una interfície que no encaixa Adaptador
Vull afegir memòria cau o mètriques sense tocar la classe Decorador
Vull controlar l'accés, retardar la càrrega o interceptar crides Proxy
Diversos processos comparteixen l'esquelet i difereixen en passos Template Method
En passar alguna cosa, diverses coses independents han de reaccionar Observador / esdeveniments d'aplicació
Un objecte es comporta diferent segons el seu estat i hi ha if per tot arreu Estat (enum amb mètodes)
Necessito desfer, encuar o registrar operacions Ordre
Diversos gestors poden atendre una petició Cadena de responsabilitat
Vull recórrer una cosa complexa com si fos una llista Iterador
Necessito una sola instància compartida Bean de Spring (no Singleton clàssic)
Crear objectes sense acoblar-me a la classe concreta Factory Method
Crear famílies coherents d'objectes Abstract Factory
L'accés a dades embruta la lògica de negoci Repositori
Un subsistema complex amb una interfície difícil Facade
Tractar igual un element i un grup d'elements Composite
Les entitats de la base de dades viatgen a l'API DTO
No puc provar una classe sense base de dades ni xarxa Inversió de dependències + Injecció

Errors Comuns i Consells

1. Aplicar un patró perquè l'acabes d'aprendre. La síndrome del martell. Si en acabar aquesta lliçó la teva propera classe té una fàbrica abstracta que produeix estratègies decorades, atura't. El patró ha d'arribar com a resposta a un dolor concret, no com a decoració.

2. Confondre el nom amb l'estructura. Anomenar PrestecFactory una classe que no crea res, o RepositoriImpl una façana. Els noms de patró són promeses: si el nom diu Decorador, qui ho llegeixi esperarà que implementi la mateixa interfície que embolcalla.

3. Començar pels patrons i no pels principis. Un projecte amb quinze patrons mal repartits és pitjor que un amb cap i SOLID respectat. Els principis s'apliquen sempre; els patrons, quan toca.

4. Estratègies sense estat convertides en jerarquies de classes. A Java 8+, una estratègia sense estat és una lambda. Comparator.comparing(Material::getTitol) és una estratègia completa en una línia. No creïs tres classes per a això.

5. Oblidar el try/catch al bucle de l'Observador. Un oient que llança excepció impedeix que s'executin els següents. És la fallada més comuna del patró i la més difícil de diagnosticar després.

6. Herència on toca composició. Si dubtes, fes servir composició. L'herència acobla la subclasse als detalls interns de la superclasse, i aquest acoblament no es veu fins que la superclasse canvia.

7. Decoradors apilats sense control. Cinc capes de decoració converteixen una traça de pila en un jeroglífic i una crida simple en cinc salts. Dues o tres capes està bé; cinc és senyal que el problema és un altre.

8. Creure que @Transactional funciona en crides internes. Ara saps per què no: és un proxy. Si this.metodeTransaccional() no obre transacció, no és un error de Spring.

9. Model anèmic per inèrcia. Generar entitats amb tots els setters «perquè l'IDE els fa» i després preguntar-se per què les regles estan duplicades en tres serveis. Comença sense setters i afegeix només els que necessitis.

10. No documentar el patró aplicat. Si CatalegAmbCache és un decorador, digues-ho al Javadoc de la classe. La persona següent ho entendrà en cinc segons en lloc de en deu minuts.

Consell final: la prova de foc d'un patró ben aplicat és que treu codi de decisió, no que l'afegeixi. Si després d'aplicar-lo hi ha més if, més classes i la mateixa rigidesa, treu-lo.

Exercicis

Exercici 1: identificar i refactoritzar

Aquest mètode existeix a BiblioTech. Identifica tots els principis que viola i tots els patrons que resoldrien cada problema; després reescriu-lo.

@Service
public class ServeiExportacio {

    @Autowired private EntityManager em;
    @Autowired private JavaMailSender correu;

    public void exportar(String format, String desti, String correuDesti) throws Exception {
        List<Material> materials = em.createQuery("select m from Material m", Material.class)
                                     .getResultList();
        String contingut;

        if (format.equals("csv")) {
            StringBuilder sb = new StringBuilder("isbn;titol;tipus\n");
            for (Material m : materials) {
                sb.append(m.getIsbn()).append(';')
                  .append(m.getTitol()).append(';');
                if (m instanceof Llibre)       sb.append("LLIBRE");
                else if (m instanceof Revista) sb.append("REVISTA");
                else if (m instanceof Dvd)     sb.append("DVD");
                sb.append('\n');
            }
            contingut = sb.toString();
        } else if (format.equals("json")) {
            contingut = new ObjectMapper().writeValueAsString(materials);
        } else {
            throw new IllegalArgumentException("Format no suportat: " + format);
        }

        Files.writeString(Path.of(desti), contingut);

        if (correuDesti != null) {
            SimpleMailMessage m = new SimpleMailMessage();
            m.setTo(correuDesti);
            m.setSubject("Cataleg BiblioTech");
            m.setText("Adjunto el cataleg en format " + format);
            correu.send(m);
        }
        System.out.println("Exportats " + materials.size() + " materials");
    }
}

Exercici 2: implementar un decorador amb control de taxa

BiblioTech consulta l'API externa de metadades, que limita a 10 peticions per minut. Superar-ho retorna HTTP 429 i bloqueja el compte durant una hora.

Implementa un decorador PassarelaAmbLimitDeTaxa que embolcalli qualsevol PassarelaMetadades i garanteixi que no se superen N peticions per minut, esperant si cal. Ha de ser segur davant la concurrència (mòdul 8) i respectar la interrupció del fil.

Exercici 3: màquina d'estats d'una reserva

Modela el cicle de vida d'una Reserva de BiblioTech amb el patró Estat fent servir un enum amb mètodes.

Estats: PENDENT (a la cua, esperant exemplar), DISPONIBLE (hi ha exemplar, l'empleat té 48 h per recollir-lo), RECOLLIDA (es va convertir en préstec), CADUCADA (van passar les 48 h), CANCELLADA (l'empleat la va anul·lar).

Regles: una reserva pendent es pot cancel·lar o passar a disponible; una de disponible es pot recollir, cancel·lar o caducar; RECOLLIDA, CADUCADA i CANCELLADA són finals; només DISPONIBLE té data límit; només PENDENT i DISPONIBLE ocupen lloc a la cua.

Escriu l'enum complet, el diagrama d'estats i la classe Reserva que el fa servir sense ni un sol switch.


Solucions

Solució 1

Violacions i patrons aplicables:

# Problema Principi violat Patró
1 La classe consulta, formata, escriu, envia correu i imprimeix Responsabilitat única Extreure col·laboradors
2 if/else sobre el format Obert/tancat Estratègia o Abstract Factory
3 Cadena d'instanceof per al tipus Obert/tancat, Liskov Polimorfisme
4 Dependència directa d'EntityManager i JavaMailSender Inversió de dependències Repositori + Port
5 @Autowired sobre camps Injecció per constructor (11-02)
6 new ObjectMapper() a cada crida Bean singleton (11-07: car de construir)
7 System.out.println Separació de responsabilitats Logging (11-07)
8 throws Exception Frontera d'errors (06-07) Excepcions específiques
9 Escriure al disc encastat Inversió de dependències Port MagatzemExportacions

Reescriptura. Primer, el problema 3 es resol on ha de ser, al domini:

public abstract class Material {
    public abstract TipusMaterial tipus();   // adeu a l'instanceof
}
public class Llibre extends Material {
    @Override public TipusMaterial tipus() { return TipusMaterial.LLIBRE; }
}

L'estratègia de format:

public interface FormatadorCataleg {
    String formatar(List<Material> materials);
    Format format();
    String extensio();
}

@Component
class FormatadorCsv implements FormatadorCataleg {
    @Override public Format format()     { return Format.CSV; }
    @Override public String extensio()   { return "csv"; }

    @Override
    public String formatar(List<Material> materials) {
        return materials.stream()
                .map(m -> String.join(";", m.getIsbn().valor(), m.getTitol(), m.tipus().name()))
                .collect(Collectors.joining("\n", "isbn;titol;tipus\n", "\n"));
    }
}

@Component
class FormatadorJson implements FormatadorCataleg {
    private final ObjectMapper mapper;               // injectat: un de sol, reutilitzat

    FormatadorJson(ObjectMapper mapper) { this.mapper = mapper; }

    @Override public Format format()    { return Format.JSON; }
    @Override public String extensio()  { return "json"; }

    @Override
    public String formatar(List<Material> materials) {
        try {
            // DTO, no entitats (12-01)
            List<MaterialDto> dtos = materials.stream().map(MaterialDto::desDe).toList();
            return mapper.writeValueAsString(dtos);
        } catch (JsonProcessingException e) {
            throw new ExportacioFallidaException("No s'ha pogut serialitzar el cataleg", e);
        }
    }
}

Els ports de sortida:

public interface MagatzemExportacions {
    URI guardar(String nom, String contingut);
}

public interface NotificadorAvisos {
    void notificar(Avis avis);
}

I el servei, que ara només orquestra:

@Service
public class ServeiExportacio {

    private static final Logger log = LoggerFactory.getLogger(ServeiExportacio.class);

    private final RepositoriMaterials materials;
    private final Map<Format, FormatadorCataleg> formatadors;
    private final MagatzemExportacions magatzem;
    private final NotificadorAvisos notificador;

    /** Spring injecta TOTS els formatadors; els indexem per format. */
    public ServeiExportacio(RepositoriMaterials materials,
                            List<FormatadorCataleg> formatadors,
                            MagatzemExportacions magatzem,
                            NotificadorAvisos notificador) {
        this.materials = materials;
        this.formatadors = formatadors.stream()
                .collect(Collectors.toUnmodifiableMap(FormatadorCataleg::format, f -> f));
        this.magatzem = magatzem;
        this.notificador = notificador;
    }

    public ResultatExportacio exportar(Format format, String nom, String correuDesti) {
        FormatadorCataleg formatador = formatadors.get(format);
        if (formatador == null) {
            throw new FormatNoSuportatException(format, formatadors.keySet());
        }

        List<Material> cataleg = materials.tots();
        String contingut = formatador.formatar(cataleg);
        URI ubicacio = magatzem.guardar(nom + "." + formatador.extensio(), contingut);

        if (correuDesti != null) {
            notificador.notificar(Avis.exportacioLlesta(correuDesti, format, ubicacio));
        }

        log.info("Exportats {} materials en format {} a {}", cataleg.size(), format, ubicacio);
        return new ResultatExportacio(cataleg.size(), ubicacio, format);
    }
}

Afegir XML és ara crear una classe FormatadorXml anotada amb @Component. Zero canvis a ServeiExportacio. Això és el principi obert/tancat a la pràctica.

Solució 2

/**
 * Decorador que limita la taxa de peticions a la passarela de metadades.
 *
 * Implementa una finestra lliscant: guarda la marca de temps de les
 * ultimes N peticions; si la mes antiga es dins de la finestra,
 * espera fins que en surti.
 *
 * ES una PassarelaMetadades i TE una PassarelaMetadades: decorador.
 */
public class PassarelaAmbLimitDeTaxa implements PassarelaMetadades {

    private static final Logger log = LoggerFactory.getLogger(PassarelaAmbLimitDeTaxa.class);

    private final PassarelaMetadades delegat;
    private final int maximPeticions;
    private final Duration finestra;
    private final Clock rellotge;                  // 10-05: injectable, per poder provar-ho

    /** Marques de temps de les ultimes peticions. Acces sempre sota el pany. */
    private final Deque<Instant> marques = new ArrayDeque<>();
    private final ReentrantLock pany = new ReentrantLock(true);   // just: ordre FIFO

    public PassarelaAmbLimitDeTaxa(PassarelaMetadades delegat, int maximPeticions,
                                   Duration finestra, Clock rellotge) {
        this.delegat = Objects.requireNonNull(delegat);
        if (maximPeticions < 1) throw new IllegalArgumentException("maximPeticions >= 1");
        this.maximPeticions = maximPeticions;
        this.finestra = Objects.requireNonNull(finestra);
        this.rellotge = Objects.requireNonNull(rellotge);
    }

    @Override
    public Optional<MetadadesMaterial> cercar(Isbn isbn) {
        adquirirPermis();                  // pot bloquejar
        return delegat.cercar(isbn);       // la crida real, ja fora del pany
    }

    private void adquirirPermis() {
        pany.lock();
        try {
            while (true) {
                Instant ara = Instant.now(rellotge);
                purgarAntigues(ara);

                if (marques.size() < maximPeticions) {
                    marques.addLast(ara);
                    return;
                }

                // La finestra esta plena: cal esperar que caduqui la mes antiga
                Instant mesAntiga = marques.peekFirst();
                Duration espera = Duration.between(ara, mesAntiga.plus(finestra));
                if (espera.isNegative() || espera.isZero()) continue;

                log.debug("Limit de taxa assolit; esperant {} ms", espera.toMillis());
                pany.unlock();                       // alliberar mentre dormim
                try {
                    Thread.sleep(espera.toMillis());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();     // regla del modul 8: restaurar la marca
                    throw new ConsultaInterrompudaException("Espera de limit de taxa interrompuda", e);
                } finally {
                    pany.lock();                     // recuperar abans de tornar al bucle
                }
            }
        } finally {
            pany.unlock();
        }
    }

    private void purgarAntigues(Instant ara) {
        Instant limit = ara.minus(finestra);
        while (!marques.isEmpty() && marques.peekFirst().isBefore(limit)) {
            marques.pollFirst();
        }
    }
}

Muntatge amb els decoradors apilats:

@Bean
PassarelaMetadades passarelaMetadades(AdaptadorMetadadesHttp http, Clock rellotge, MeterRegistry registre) {
    return new PassarelaAmbMetriques(                    // 3r: mesura el temps total (inclosa l'espera)
             new PassarelaAmbLimitDeTaxa(                // 2n: no supera 10 per minut
               new PassarelaAmbCache(http, Duration.ofHours(24)),   // 1r: la memoria cau evita peticions
               10, Duration.ofMinutes(1), rellotge));
}

Fixa't en l'ordre: la memòria cau va dins del limitador, de manera que un encert de memòria cau no consumeix quota. Al revés, cada consulta desada gastaria una petició del pressupost sense arribar a la xarxa. Aquest detall d'ordre és la raó per la qual el patró Decorador exigeix pensar, no només apilar.

Prova, aprofitant el Clock injectable:

@Test
void esperaQuanSeSuperaElLimit() {
    var delegat = new PassarelaFalsa();
    var rellotge = Clock.fixed(Instant.parse("2026-03-20T10:00:00Z"), ZoneOffset.UTC);
    var passarela = new PassarelaAmbLimitDeTaxa(delegat, 2, Duration.ofSeconds(1), rellotge);

    long inici = System.nanoTime();
    passarela.cercar(Isbn.de("978-0000000001"));
    passarela.cercar(Isbn.de("978-0000000002"));
    passarela.cercar(Isbn.de("978-0000000003"));   // la tercera ha d'esperar ~1 s
    long ms = (System.nanoTime() - inici) / 1_000_000;

    assertThat(ms).isGreaterThanOrEqualTo(900);
    assertThat(delegat.crides()).isEqualTo(3);
}

Solució 3

stateDiagram-v2
    [*] --> PENDENT : reservar
    PENDENT --> DISPONIBLE : arriba exemplar
    PENDENT --> CANCELLADA : cancellar
    DISPONIBLE --> RECOLLIDA : recollir
    DISPONIBLE --> CANCELLADA : cancellar
    DISPONIBLE --> CADUCADA : passen 48 h
    RECOLLIDA --> [*]
    CADUCADA --> [*]
    CANCELLADA --> [*]
public enum EstatReserva {

    /** A la cua, esperant que hi hagi un exemplar lliure. */
    PENDENT {
        @Override public boolean potCancellarse()     { return true; }
        @Override public boolean ocupaLlocACua()      { return true; }
        @Override public EstatReserva enHaverExemplar() { return DISPONIBLE; }
    },

    /** Hi ha exemplar apartat; l'empleat disposa de 48 hores. */
    DISPONIBLE {
        @Override public boolean potCancellarse()  { return true; }
        @Override public boolean potRecollirse()   { return true; }
        @Override public boolean potCaducar()      { return true; }
        @Override public boolean ocupaLlocACua()   { return true; }
        @Override public boolean teDataLimit()     { return true; }
        @Override public EstatReserva enRecollir() { return RECOLLIDA; }
        @Override public EstatReserva enCaducar()  { return CADUCADA; }
    },

    RECOLLIDA  { @Override public boolean esFinal() { return true; } },
    CADUCADA   { @Override public boolean esFinal() { return true; } },
    CANCELLADA { @Override public boolean esFinal() { return true; } };

    // --- Consultes: per defecte, res no esta permes ---
    public boolean potCancellarse() { return false; }
    public boolean potRecollirse()  { return false; }
    public boolean potCaducar()     { return false; }
    public boolean ocupaLlocACua()  { return false; }
    public boolean teDataLimit()    { return false; }
    public boolean esFinal()        { return false; }

    // --- Transicions: per defecte, invalides ---
    public EstatReserva enHaverExemplar() { throw new TransicioInvalidaException(this, "haver exemplar"); }
    public EstatReserva enRecollir()      { throw new TransicioInvalidaException(this, "recollir"); }
    public EstatReserva enCaducar()       { throw new TransicioInvalidaException(this, "caducar"); }

    /** Cancellar es l'unica transicio comuna a diversos estats. */
    public EstatReserva enCancellar() {
        if (!potCancellarse()) throw new TransicioInvalidaException(this, "cancellar");
        return CANCELLADA;
    }
}

I l'entitat, sense ni un sol switch ni if sobre l'estat:

@Entity
public class Reserva {

    private static final int HORES_PER_RECOLLIR = 48;

    @Id @GeneratedValue private Long id;
    @ManyToOne(fetch = FetchType.LAZY) private Material material;
    @ManyToOne(fetch = FetchType.LAZY) private Empleat empleat;
    @Enumerated(EnumType.STRING) private EstatReserva estat;
    private Instant dataSollicitud;
    private Instant dataLimitRecollida;       // nomes te sentit a DISPONIBLE
    @Version private long version;            // bloqueig optimista (11-03)

    protected Reserva() { }                   // exigit per JPA

    public static Reserva nova(Material material, Empleat empleat, Clock rellotge) {
        Reserva r = new Reserva();
        r.material = material;
        r.empleat = empleat;
        r.estat = EstatReserva.PENDENT;
        r.dataSollicitud = Instant.now(rellotge);
        return r;
    }

    public void assignarExemplar(Clock rellotge) {
        this.estat = estat.enHaverExemplar();         // valida i transita en una linia
        this.dataLimitRecollida = Instant.now(rellotge).plus(HORES_PER_RECOLLIR, ChronoUnit.HOURS);
    }

    public void recollir() {
        this.estat = estat.enRecollir();
        this.dataLimitRecollida = null;
    }

    public void cancellar() {
        this.estat = estat.enCancellar();
        this.dataLimitRecollida = null;
    }

    /** Retorna true si efectivament va caducar. Idempotent: si no escau, no fa res. */
    public boolean caducarSiEscau(Clock rellotge) {
        if (!estat.potCaducar()) return false;
        if (Instant.now(rellotge).isBefore(dataLimitRecollida)) return false;
        this.estat = estat.enCaducar();
        this.dataLimitRecollida = null;
        return true;
    }

    public boolean ocupaLlocACua() { return estat.ocupaLlocACua(); }
}

I el procés periòdic que caduca reserves queda trivial:

@Scheduled(cron = "0 */15 * * * *")     // cada 15 minuts
@Transactional
public void caducarReservesVencudes() {
    int caducades = 0;
    for (Reserva r : repositori.enEstat(EstatReserva.DISPONIBLE)) {
        if (r.caducarSiEscau(rellotge)) {
            caducades++;
            esdeveniments.publishEvent(new ReservaCaducada(r.getId()));   // Observador
        }
    }
    log.info("Reserves caducades: {}", caducades);
}

El que ha guanyat el disseny: la màquina d'estats viu en un únic fitxer de 40 línies, qualsevol transició invàlida llança una excepció clara amb l'estat d'origen i l'acció intentada, afegir un estat SUSPESA és afegir una constant, i és impossible deixar una reserva en un estat incoherent des de qualsevol punt del sistema.

Conclusió

Tots els deutes del curs estan saldats.

Vas començar pel que de debò importa: els principis. Els cinc de SOLID, cadascun amb un abans i un després de BiblioTech: la responsabilitat única que va partir aquell GestorPrestecs amb set raons per canviar; l'obert/tancat que explica per què el polimorfisme de 03-06 era més que un truc de sintaxi; la substitució de Liskov amb la seva violació més didàctica —el MaterialDeReferencia que llançava UnsupportedOperationException— i la seva solució real, que no era arreglar la subclasse sinó redissenyar la jerarquia; la segregació d'interfícies que ja practicaves des de 04-01 amb Prestable i Notificable; i la inversió de dependències, amb el matís que gairebé ningú no explica: Spring et dona DI i IoC, però el DIP depèn d'on posis tu la interfície. A més de DRY sobre coneixement i no sobre text, KISS, YAGNI i la llei de Demeter amb la seva excepció per a les API fluides.

Després, el catàleg, sempre amb el mateix esquema: problema, estructura, codi real de BiblioTech i —el que gairebé cap material no inclou— quan no fer-lo servir. Els creacionals, amb el Builder que es va prometre a 03-04 per fi implementat per a Prestec i contrastat honestament amb record i amb @Builder de Lombok, i amb el Singleton clàssic posat al seu lloc: una variable global disfressada que el bean de Spring fa millor en les set dimensions que importen. Els estructurals, amb l'Adaptador que aïlla el ClientMetadades extern darrere d'un port del domini i les seves tres traduccions —tipus, excepcions i semàntica—; el Decorador promès des de 07-03, amb els fluxos d'E/S com a cas canònic i el CatalegAmbCache com a cas propi, inclosa la lliçó que l'ordre d'apilament és una decisió de disseny; i el Proxy, que tanca el cercle de 10-03 i explica d'una vegada per què @Transactional no funciona en autoinvocacions ni en mètodes privats.

I els de comportament, on hi havia els deutes més antics. L'Estratègia que feies servir des del mòdul 4 amb les ReglaTarifa i els Comparator, ara amb nom i amb la versió Spring que la fa gairebé invisible. El Template Method promès a 04-02, amb la comparació que importa: herència enfront de composició, i per què la preferència moderna és la segona. L'Observador amb els seus OientDevolucio, el try/catch dins del bucle que gairebé tothom oblida, i els esdeveniments d'aplicació de Spring amb @TransactionalEventListener(AFTER_COMMIT) com la seva versió moderna i correcta. L'Estat, resolt amb l'enum de mètodes abstractes que és la implementació més neta que permet Java. L'Ordre, que ja tenies a la pila de desfer de 05-08 i en cada Runnable del mòdul 8. I la Cadena de responsabilitat i l'Iterador, que reprenen 05-02 i que tornaràs a veure a la cadena de filtres de 12-07.

Coneixes a més els patrons d'empresa que BiblioTech fa servir sense que els escrivissis: Repositori, Unitat de treball, DTO, Servei d'aplicació, Objecte de valor, Especificació. I els antipatrons, inclòs el debat honest sobre el model anèmic, en què BiblioTech pren partit de manera explícita: entitats raonablement riques que protegeixen els seus propis invariants, serveis d'aplicació per al que creua agregats, transaccions o infraestructura.

Per damunt de tot el catàleg, queda l'advertiment amb què va començar la lliçó, que és l'única cosa que no has d'oblidar: un patró que no respon a un problema real és deute tècnic amb bon nom. L'objectiu no és reconèixer patrons al teu codi; és que el teu codi sigui fàcil de canviar. Els patrons són un mitjà, i de vegades el mitjà correcte és un if.

BiblioTech té ara arquitectura, mòduls, fronteres verificades pel compilador i un disseny intern amb nom propi. Li continua faltant una cosa elemental: ningú no el pot fer servir. No hi ha ni una interfície per la qual la Marta Ruiz pugui prestar un llibre sense escriure codi Java.

La lliçó següent ho arregla pel camí més directe i més subestimat: una aplicació de consola. No el menú amb Scanner del mòdul 2, sinó una CLI professional —amb subordres, opcions tipades, ajuda automàtica, codis de sortida amb significat, sortida componible amb canonades i cancel·lació neta— que un administrador de sistemes de Nexus Software pugui ficar en un cron sense queixar-se.

Curs de Programació en Java

Mòdul 1: Introducció a Java

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats