Les dues lliçons de fàbriques van resoldre quina classe instanciar. Builder ataca un problema diferent: la classe és clara —és una Comanda, no hi ha dubte—, però muntar-la és un suplici: moltes dades, la majoria opcionals, combinacions invàlides, i un constructor que ha anat engreixant-se fins a tornar-se il·legible. En aquesta lliçó resoldrem el constructor telescòpic de Comanda que arrosseguem des de la introducció del mòdul, veurem l'estructura GoF original (amb el seu Director) i la variant fluida moderna que domina el Java actual, i aprendrem a usar build() com a frontera de validació i immutabilitat.
Contingut
- El problema a PideYa: el constructor telescòpic
- Intenció del patró
- Estructura GoF: Builder i Director
- La variant fluida moderna (la que usaràs el 95% de les vegades)
- Implementació Java completa del
Comanda.Builder - Immutabilitat i validació a
build() - El Director en la variant moderna
- Quan usar-lo i quan no
- Errors comuns, exercicis i conclusió
El problema a PideYa: el constructor telescòpic
Una Comanda de PideYa necessita, per néixer: client i restaurant (sempre), les seves línies (almenys una), adreça de lliurament (llevat de recollida al local), i opcionalment franja horària, instruccions per al repartidor ("portal 3, no trucar al timbre"), codi promocional, coberts sí/no i telèfon de contacte alternatiu. L'evolució típica del constructor al llarg dels sprints:
// Sprint 3
public Comanda(Client client, Restaurant restaurant, List<LiniaComanda> linies) { ... }
// Sprint 7: lliurament a domicili
public Comanda(Client client, Restaurant restaurant, List<LiniaComanda> linies,
Adreca adrecaLliurament) { ... }
// Sprint 12: la criatura ja fa por
public Comanda(Client client, Restaurant restaurant, List<LiniaComanda> linies,
Adreca adrecaLliurament, FranjaHoraria franja, String instruccions,
String codiPromocional, boolean incloureCoberts, String telefonContacte) { ... }D'això se'n diu constructor telescòpic: per cobrir les combinacions d'opcionals s'acaba amb una escala de constructors solapats (o un de gegant al qual es crida amb nulls). Les crides resultants són criptogrames:
Comanda c = new Comanda(client, rest, linies, adreca, null, null, "PIZZA10", true, null);
// Que es "PIZZA10"? Que significa true? Quin dels null era la franja?Els dolors, enumerats:
- Il·legibilitat: els arguments posicionals no diuen què són; els booleans i
nulls són mines. - Fragilitat: dos paràmetres contigus del mateix tipus (
instruccionsicodiPromocional, tots dosString) s'intercanvien sense que el compilador digui res. - Explosió de combinacions: amb 6 opcionals caldrien fins a 64 constructors per cobrir totes les variants amb signatures netes.
- Sense lloc per validar: regles com "domicili requereix adreça" o "màxim un codi promocional" acaben repartides pels cridadors.
- L'alternativa JavaBeans (constructor buit + setters) és encara pitjor: l'objecte travessa estats intermedis invàlids, qualsevol pot mutar-lo després, i impossibilita la immutabilitat.
Intenció del patró
Intenció (GoF): separar la construcció d'un objecte complex de la seva representació, de manera que el mateix procés de construcció pugui crear representacions diferents.
La idea: en comptes de lliurar totes les dades de cop a un constructor, s'acumulen pas a pas en un objecte intermedi —el builder— que sap rebre-les amb nom, en qualsevol ordre, i que al final fabrica el producte d'una peça, validat i complet. La segona meitat de la intenció GoF ("el mateix procés, representacions diferents") pertany a la variant clàssica amb Director; la variant moderna es queda sobretot amb la primera meitat. Vegem-les totes dues.
Estructura GoF: Builder i Director
classDiagram
class Director {
-builder : Builder
+construir()
}
class Builder {
<<interface>>
+posarBase()
+posarLinies()
+posarLliurament()
+obtenirResultat()
}
class BuilderConcret1 {
+posarBase()
+posarLinies()
+posarLliurament()
+obtenirResultat() Producte1
}
class BuilderConcret2 {
+posarBase()
+posarLinies()
+posarLliurament()
+obtenirResultat() Producte2
}
Director o--> Builder : dirigeix
Builder <|.. BuilderConcret1
Builder <|.. BuilderConcret2
BuilderConcret1 ..> Producte1 : crea
BuilderConcret2 ..> Producte2 : crea
| Rol GoF | Paper |
|---|---|
| Builder | Interfície amb les operacions de construcció parcials |
| ConcreteBuilder | Implementa els passos i sap muntar UNA representació; exposa obtenirResultat() |
| Director | Coneix la recepta (quins passos i en quin ordre), sense saber quina representació surt |
| Product | L'objecte complex resultant |
La clau del repartiment: el Director posseeix l'algorisme de construcció i el Builder posseeix la materialització de cada pas. Amb la mateixa recepta i builders diferents s'obtenen productes diferents. A PideYa això encaixa, per exemple, a generar el resum d'una comanda en diversos formats: un DirectorResumComanda recorre sempre els mateixos passos (capçalera, línies, totals, dades de lliurament) i, segons que li endollis un BuilderResumHtml, un BuilderResumText (per a SMS) o un BuilderTiquetCuina, surt una representació o una altra.
Dit això, siguem francs: en el Java quotidià aquesta forma completa, amb interfície i intercanvi de builders, és minoritària. El que trobaràs pertot arreu és el seu descendent fluid.
La variant fluida moderna (la que usaràs el 95% de les vegades)
La variant popularitzada per Effective Java (Bloch) simplifica: un únic builder, classe estàtica interna del producte, amb mètodes que retornen this per encadenar-se (interfície fluida), sense interfície ni Director. L'objectiu ja no és "mateixa recepta, diverses representacions" sinó domar els constructors telescòpics amb llegibilitat, immutabilitat i validació. Així s'usa el que escriurem:
Comanda comanda = Comanda.builder(client, restaurant)
.linia(margarita, 2)
.linia(aigua, 1)
.lliuramentADomicili(adrecaCasa)
.franja(FranjaHoraria.de(21, 0, 21, 30))
.instruccions("Portal 3, no trucar al timbre")
.codiPromocional("PIZZA10")
.ambCoberts()
.build();Compara amb el criptograma del principi: cada dada porta el seu nom, els opcionals només apareixen si existeixen, l'ordre és lliure, i el compilador impedeix confondre l'adreça amb el telèfon. Aquest estil te'l creuaràs cada dia: StringBuilder, Stream.builder(), HttpRequest.newBuilder() al JDK; UriComponentsBuilder a Spring; i les anotacions @Builder de Lombok o els records amb builders generats en moltes bases de codi.
Implementació Java completa del Comanda.Builder
public final class Comanda {
// ---- Estat: tot final. Una Comanda, un cop creada, es immutable ----
private final Client client;
private final Restaurant restaurant;
private final List<LiniaComanda> linies;
private final TipusLliurament tipusLliurament; // DOMICILI o RECOLLIDA
private final Adreca adrecaLliurament; // nomes si DOMICILI
private final FranjaHoraria franja; // opcional
private final String instruccions; // opcional
private final String codiPromocional; // opcional
private final boolean incloureCoberts;
private final String telefonContacte; // opcional
/** Constructor PRIVAT: l'unica porta d'entrada es el builder. */
private Comanda(Builder b) {
this.client = b.client;
this.restaurant = b.restaurant;
this.linies = List.copyOf(b.linies); // copia defensiva i immutable
this.tipusLliurament = b.tipusLliurament;
this.adrecaLliurament = b.adrecaLliurament;
this.franja = b.franja;
this.instruccions = b.instruccions;
this.codiPromocional = b.codiPromocional;
this.incloureCoberts = b.incloureCoberts;
this.telefonContacte = b.telefonContacte;
}
/** Punt d'entrada: els OBLIGATORIS s'exigeixen ja aqui. */
public static Builder builder(Client client, Restaurant restaurant) {
return new Builder(client, restaurant);
}
// ---- getters (sense setters: immutable) ----
public BigDecimal getTotalSenseImpostos() {
return linies.stream()
.map(LiniaComanda::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// ... resta de getters ...
// =====================================================================
public static final class Builder {
// Obligatoris: final, arriben pel constructor del builder
private final Client client;
private final Restaurant restaurant;
// Acumulables i opcionals: mutables DURANT la construccio
private final List<LiniaComanda> linies = new ArrayList<>();
private TipusLliurament tipusLliurament = TipusLliurament.RECOLLIDA; // valor per defecte
private Adreca adrecaLliurament;
private FranjaHoraria franja;
private String instruccions;
private String codiPromocional;
private boolean incloureCoberts = false;
private String telefonContacte;
private Builder(Client client, Restaurant restaurant) {
this.client = Objects.requireNonNull(client, "client");
this.restaurant = Objects.requireNonNull(restaurant, "restaurant");
}
public Builder linia(Producte producte, int quantitat) {
linies.add(new LiniaComanda(producte, quantitat));
return this; // <- el que permet encadenar
}
public Builder lliuramentADomicili(Adreca adreca) {
this.tipusLliurament = TipusLliurament.DOMICILI;
this.adrecaLliurament = Objects.requireNonNull(adreca, "adreca");
return this;
}
public Builder recollidaAlLocal() {
this.tipusLliurament = TipusLliurament.RECOLLIDA;
this.adrecaLliurament = null;
return this;
}
public Builder franja(FranjaHoraria franja) { this.franja = franja; return this; }
public Builder instruccions(String text) { this.instruccions = text; return this; }
public Builder codiPromocional(String codi) { this.codiPromocional = codi; return this; }
public Builder ambCoberts() { this.incloureCoberts = true; return this; }
public Builder telefonContacte(String telefon) { this.telefonContacte = telefon; return this; }
/** La frontera: valida TOT i lliura el producte acabat. */
public Comanda build() {
if (linies.isEmpty()) {
throw new IllegalStateException("Una comanda necessita almenys una linia");
}
if (tipusLliurament == TipusLliurament.DOMICILI && adrecaLliurament == null) {
throw new IllegalStateException("El lliurament a domicili requereix adreca");
}
if (franja != null && !restaurant.reparteixEn(franja)) {
throw new IllegalStateException("El restaurant no reparteix en aquesta franja");
}
return new Comanda(this);
}
}
}Punts fins del codi, un a un:
- Constructor privat de
Comanda: ningú no pot fabricar una comanda saltant-se el builder (mateixa jugada de "tancar la porta" que a Singleton, amb una altra finalitat). - Obligatoris al constructor del builder, opcionals com a mètodes: impossible oblidar client o restaurant; impossible embrutar la crida amb
nulls pel que no s'usa. return this: cada mètode retorna el mateix builder; és l'únic que cal per a la fluïdesa de l'encadenat.- Mètodes amb semàntica, no només setters:
lliuramentADomicili(adreca)irecollidaAlLocal()agrupen canvis coherents (tipus + adreça) i fan inexpressables diversos estats absurds; millor que unsetTipusLliurament()i unsetAdreca()solts. List.copyOfal constructor: còpia defensiva; encara que algú reutilitzi el builder després, la comanda ja creada no canvia.
Immutabilitat i validació a build()
Aquestes dues paraules són la meitat del valor del patró, així que mereixen la seva secció:
Immutabilitat: el builder és mutable mentre dura la construcció (per a això hi és), però el producte neix complet i congelat: tots els camps final, sense setters, col·leccions copiades. La conseqüència pràctica a PideYa és enorme: una Comanda pot passar pel càlcul de promocions, el cobrament i les notificacions —fins i tot en fils diferents— amb la garantia que ningú no l'altera pel camí. El patró resol així la tensió clàssica "vull objectes immutables però amb molts opcionals": mutabilitat confinada a la bastida, immutabilitat a l'edifici.
Validació a build(): build() és la frontera única entre "dades soltes" i "comanda vàlida". Totes les regles d'integritat —almenys una línia, domicili ⇒ adreça, franja compatible amb el restaurant— es comproven allà, una sola vegada, en un sol lloc. La garantia resultant val or: si existeix una Comanda, és vàlida; cap codi posterior no necessita recomprovar. I les regles de validació creuada (les que impliquen diversos camps alhora) per fi tenen una llar natural, cosa que ni el constructor telescòpic ni els setters oferien.
Nota d'abast: parlem de validació estructural de l'objecte. Les regles de negoci dinàmiques (el codi promocional és vigent? el restaurant és obert ara?) pertanyen als serveis de domini, no al builder, que no ha de carregar amb dependències de repositoris o rellotges.
El Director en la variant moderna
I el Director, s'ha perdut? No: s'ha transformat. La seva essència —una recepta reutilitzable amb nom— continua sent útil quan certes combinacions de passos es repeteixen. A PideYa, les "comandes típiques" són receptes:
/** Director modern: encapsula receptes de construccio frequents. */
public class ReceptesComanda {
/** El "menu del dia" d'un restaurant, llest en una crida. */
public static Comanda menuDelDia(Client client, Restaurant rest, Adreca adr) {
Comanda.Builder builder = Comanda.builder(client, rest)
.lliuramentADomicili(adr)
.ambCoberts();
rest.getMenuDelDia().forEach(prod -> builder.linia(prod, 1));
return builder.build();
}
/** Comanda d'empresa: recollida, sense coberts, factura a l'empresa. */
public static Comanda comandaEmpresa(Client client, Restaurant rest, List<Producte> prods) {
Comanda.Builder builder = Comanda.builder(client, rest)
.recollidaAlLocal()
.instruccions("Comanda d'empresa - preguntar per recepcio");
prods.forEach(p -> builder.linia(p, 1));
return builder.build();
}
}És el mateix repartiment de papers GoF (la recepta separada dels passos), sense la cerimònia d'interfícies que aquí no compra res. Quan a la funció "repetir comanda" necessitem construir una comanda a partir d'una altra, veurem que hi ha una alternativa encara més directa: la següent lliçó.
Quan usar-lo i quan no
Usa'l quan:
- Un constructor supera els ~4 paràmetres o barreja diversos del mateix tipus (llindar orientatiu, no dogma).
- Hi ha diversos opcionals amb combinacions lliures: és el cas exacte de
Comanda. - Vols immutabilitat + validació centralitzada en objectes amb moltes dades.
- Construeixes una cosa incremental per naturalesa (una consulta, un informe, una petició HTTP).
- (Forma GoF amb Director) La mateixa seqüència de construcció ha de produir representacions diferents.
No l'usis quan:
- La classe té 2-3 camps obligatoris i cap d'opcional: un constructor normal —o un
recordde Java— és més curt, més clar i suficient. Un builder allà és cerimònia pura (sobreenginyeria). - El problema és quina classe instanciar, no com muntar-la: això són les fàbriques.
- Només vols paràmetres amb nom: valora abans si un
recordamb mètodeswith...o paràmetres agrupats en un objecte petit resolen el cas amb menys codi.
Relació amb altres patrons (només menció): una Abstract Factory pot retornar productes que internament munta un builder; el Director clàssic és parent de Template Method (recepta fixa, passos variables); Composite es construeix sovint amb builders; i Prototype és l'alternativa quan el punt de partida és un objecte existent i no dades soltes.
Errors Comuns i Consells
- Builder sense validació: un
build()que només fanewmalbarata la meitat del patró. Si no hi ha res a validar ni immutabilitat a protegir, potser no necessitaves builder. - Builder mutable com a substitut de setters: passar el builder d'una banda a l'altra del sistema i cridar-li
build()tres vegades en llocs diferents reintrodueix l'objecte a mig fer que volíem evitar. El builder ha de viure poc: es crea, s'omple i mor albuild(). - Oblidar la còpia defensiva de col·leccions: sense
List.copyOf, qui conservi la llista original pot mutar la comanda "immutable" des de fora. Error silenciós i clàssic. - Reutilitzar un builder per a diversos productes sense pensar-ho: després de
build(), el builder reté el seu estat; un segonbuild()crea una altra comanda igual (de vegades es vol, sovint no). Decideix i documenta la política; en cas de dubte, un builder = un producte. - Booleans posicionals disfressats:
builder.coberts(true)repeteix el problema del telescòpic en miniatura. MillorambCoberts()/ res. - Consell: quan un grup de paràmetres viatja sempre junt (carrer, número, pis, ciutat...), no els donis un builder a tots: extreu primer l'objecte
Adreca. Molts "constructors telescòpics" són en realitat objectes de valor per descobrir.
Exercicis
Exercici 1: builder per a Factura
La Factura de PideYa als restaurants té d'obligatoris restaurant, periode i quantiaComissions, i d'opcionals descomptePerVolum (BigDecimal), notes (String) i iban alternatiu de cobrament. Regla creuada: si hi ha descomptePerVolum, no pot superar el 20% de quantiaComissions. Escriu la classe immutable amb el seu builder fluid.
Exercici 2: trobar les fallades del builder
Aquest builder va passar una revisió de codi amb tres fallades serioses. Localitza-les:
public class ReservaTaula {
public String restaurant;
public LocalDateTime data;
public int comensals;
public static class Builder {
private final ReservaTaula r = new ReservaTaula();
public Builder restaurant(String nom) { r.restaurant = nom; return this; }
public Builder data(LocalDateTime d) { r.data = d; return this; }
public Builder comensals(int n) { r.comensals = n; return this; }
public ReservaTaula build() { return r; }
}
}Exercici 3: recepta de Director
Escriu, usant Comanda.builder(...) de la lliçó, la recepta reposicioOficina(Client, Restaurant, Adreca): comanda a domicili per a l'oficina, amb 10 unitats d'aigua i 5 de cafè (assumeix Producte AIGUA i CAFE disponibles), franja de 9:00 a 9:30, instruccions "Recepcio, planta 2" i sense coberts.
Solucions
Solució 1:
public final class Factura {
private final Restaurant restaurant;
private final Periode periode;
private final BigDecimal quantiaComissions;
private final BigDecimal descomptePerVolum; // pot ser null
private final String notes; // pot ser null
private final String iban; // pot ser null
private Factura(Builder b) {
this.restaurant = b.restaurant;
this.periode = b.periode;
this.quantiaComissions = b.quantiaComissions;
this.descomptePerVolum = b.descomptePerVolum;
this.notes = b.notes;
this.iban = b.iban;
}
public static Builder builder(Restaurant r, Periode p, BigDecimal comissions) {
return new Builder(r, p, comissions);
}
public static final class Builder {
private final Restaurant restaurant;
private final Periode periode;
private final BigDecimal quantiaComissions;
private BigDecimal descomptePerVolum;
private String notes;
private String iban;
private Builder(Restaurant r, Periode p, BigDecimal comissions) {
this.restaurant = Objects.requireNonNull(r);
this.periode = Objects.requireNonNull(p);
this.quantiaComissions = Objects.requireNonNull(comissions);
}
public Builder descomptePerVolum(BigDecimal d) { this.descomptePerVolum = d; return this; }
public Builder notes(String n) { this.notes = n; return this; }
public Builder iban(String iban) { this.iban = iban; return this; }
public Factura build() {
if (descomptePerVolum != null) {
BigDecimal topall = quantiaComissions.multiply(new BigDecimal("0.20"));
if (descomptePerVolum.compareTo(topall) > 0) {
throw new IllegalStateException("El descompte supera el 20% de les comissions");
}
}
return new Factura(this);
}
}
}Solució 2: (a) ReservaTaula té els camps públics i mutables i cap constructor privat: qualsevol pot crear-la buida amb new ReservaTaula() o mutar-la després del build(): no hi ha immutabilitat ni porta única; (b) el builder muta directament la instància final des del primer moment: si el builder s'abandona a mitges, ha existit un objecte invàlid, i dos build() retornen el mateix objecte compartit (àlies accidental); (c) build() no valida res: es pot construir una reserva sense restaurant, amb data passada o amb 0 comensals. Correcció: camps privats final, constructor privat que copia del builder, i validació d'obligatoris i rangs a build().
Solució 3:
public static Comanda reposicioOficina(Client client, Restaurant rest, Adreca oficina) {
return Comanda.builder(client, rest)
.linia(AIGUA, 10)
.linia(CAFE, 5)
.lliuramentADomicili(oficina)
.franja(FranjaHoraria.de(9, 0, 9, 30))
.instruccions("Recepcio, planta 2")
.build(); // sense ambCoberts(): el defecte ja es false
}Conclusió
Builder tanca el flanc que les fàbriques no cobrien: quan la dificultat no és a triar la classe sinó a muntar-la, la construcció pas a pas amb mètodes anomenats elimina el constructor telescòpic, i el build() final converteix el muntatge en una frontera de validació i immutabilitat —si existeix una Comanda, és vàlida i ningú no la podrà corrompre—. Ja distingeixes a més la forma GoF (Director amb recepta + builders intercanviables, útil per a múltiples representacions) de la variant fluida interna que domina el Java modern, i la seva regla de proporcionalitat: per sota de quatre camps, probablement sobra.
Queda una darrera manera de portar objectes al món, i és la més diferent de totes: no triar classe ni muntar peça a peça, sinó copiar un objecte que ja existeix. És just el que demana la funció "repetir la meva darrera comanda" de PideYa, i té més arestes en Java de les que aparenta. Ens veiem a Prototype.
Curs de Patrons de Disseny de Programari
Mòdul 1: Introducció als Patrons de Disseny
- Què són els Patrons de Disseny?
- Història i Origen dels Patrons de Disseny
- Principis de Disseny: SOLID i Altres Fonaments
- UML Essencial per Entendre Patrons
- Classificació dels Patrons de Disseny
- Avantatges i Desavantatges d'Usar Patrons de Disseny
Mòdul 2: Patrons Creacionals
- Introducció als Patrons Creacionals
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa i Elecció de Patrons Creacionals
Mòdul 3: Patrons Estructurals
- Introducció als Patrons Estructurals
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa i Elecció de Patrons Estructurals
Mòdul 4: Patrons de Comportament
- Introducció als Patrons de Comportament
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa i Elecció de Patrons de Comportament
Mòdul 5: Aplicació de Patrons de Disseny
- Com Seleccionar el Patró Adequat
- Exemples Pràctics d'Ús de Patrons
- Patrons de Disseny en Projectes Reals
- Refactorització Usant Patrons de Disseny
- Antipatrons: Quan els Patrons es Tornen un Problema
Mòdul 6: Patrons de Disseny Avançats
- Patrons de Disseny en Arquitectures Modernes
- Patrons de Disseny en Microserveis
- Patrons de Disseny en Sistemes Distribuïts
- Patrons de Concurrència
- Patrons de Disseny en Desenvolupament Àgil
