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
- Què és un patró i què no
- El catàleg GoF i el seu context històric
- Principis abans que patrons
- SOLID: responsabilitat única (S)
- SOLID: obert/tancat (O)
- SOLID: substitució de Liskov (L)
- SOLID: segregació d'interfícies (I)
- SOLID: inversió de dependències (D)
- DRY, KISS, YAGNI i la llei de Demeter
- Patrons creacionals: Singleton
- Patrons creacionals: Factory Method i Abstract Factory
- Patrons creacionals: Builder
- Patrons creacionals: Prototype
- Patrons estructurals: Adaptador
- Patrons estructurals: Decorador
- Patrons estructurals: Proxy
- Patrons estructurals: Facade i Composite
- Patrons de comportament: Estratègia
- Patrons de comportament: Template Method
- Patrons de comportament: Observador
- Patrons de comportament: Estat
- Patrons de comportament: Ordre
- Patrons de comportament: Cadena de responsabilitat i Iterador
- Patrons arquitectònics i d'empresa
- Antipatrons
- Taula final: problema → patró candidat
- Errors Comuns i Consells
- Exercicis
- Conclusió
- 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, unifestà 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.
- 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:
- 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. - Alguns han envellit malament. El Singleton clàssic avui es considera majoritàriament un antipatró (ho veurem).
Observer/Observabledejava.utilestan obsolets des de Java 9.
- 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
- 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.
- 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.
- SOLID: substitució de Liskov (L)
Si
Sés subtipus deT, s'ha de poder substituirTperSsense 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 PrestableI 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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 | Sí | Afegir comportament |
| Adaptador | No (aquesta és la gràcia) | Canviar la interfície |
| Proxy | Sí | 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.
- 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:
- L'autoinvocació no funciona. Si
metodeA()cridathis.metodeB()i nomésmetodeBés@Transactional, no hi ha transacció: la cridathis.no passa pel proxy. - Els mètodes
privateifinalno 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 |
- 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);
}
}
- 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ó.
- 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.
- 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.
- 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.
- 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é.
- 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)) { … }
- 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.
}
- 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.
- 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
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
