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

  1. El problema a PideYa: el constructor telescòpic
  2. Intenció del patró
  3. Estructura GoF: Builder i Director
  4. La variant fluida moderna (la que usaràs el 95% de les vegades)
  5. Implementació Java completa del Comanda.Builder
  6. Immutabilitat i validació a build()
  7. El Director en la variant moderna
  8. Quan usar-lo i quan no
  9. 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 (instruccions i codiPromocional, tots dos String) 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) i recollidaAlLocal() agrupen canvis coherents (tipus + adreça) i fan inexpressables diversos estats absurds; millor que un setTipusLliurament() i un setAdreca() solts.
  • List.copyOf al 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 record de 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 record amb mètodes with... 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 fa new malbarata 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 al build().
  • 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 segon build() 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. Millor ambCoberts() / 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

Mòdul 2: Patrons Creacionals

Mòdul 3: Patrons Estructurals

Mòdul 4: Patrons de Comportament

Mòdul 5: Aplicació de Patrons de Disseny

Mòdul 6: Patrons de Disseny Avançats

Mòdul 7: Recursos Addicionals i Conclusió

© Copyright 2026. Tots els drets reservats