Il Builder Pattern come alternativa ai costruttori
In questo post vediamo assieme l’utilizzo del Builder Pattern come alternativa all’utilizzo dei costruttori in overloading. L’obiettivo è quello di rimpiazzare costruttori poco leggibili e scomodi da utilizzare con una classe, il builder appunto, che agevoli la costruzione di oggetti articolati e potenzialmente complessi.
Nell’esempio faremo uso di una classe chiamata Seminario
che rappresenta un corso di formazione tenuto in qualche sede in una specifica data.
package it.cosenonjaviste.builderpattern.senzapattern; import java.text.DateFormat; import java.util.Date; public class Seminario { public enum Luogo { SEDE, CLIENTE } private static DateFormat dt = DateFormat.getDateInstance(); private String argomento; private Luogo luogo; private Date data; private Integer numeroPartecipanti; private Integer giorniPromemoria; private Integer durataInOre; private Integer numeroMassimoPartecipanti; public Seminario(String argomento){ this.argomento = argomento; } public Seminario(String argomento, Luogo luogo, Date data) { this(argomento); this.luogo = luogo; this.data = data; } public Seminario(String argomento, Luogo luogo, Date data, Integer giorniPromemoria) { this(argomento, luogo, data); this.giorniPromemoria = giorniPromemoria; } public Seminario(String argomento, Luogo luogo, Date data, Integer giorniPromemoria, Integer numeroPartecipanti) { this(argomento); this.data = data; this.giorniPromemoria = giorniPromemoria; this.numeroPartecipanti = numeroPartecipanti; } public Seminario(String argomento, Luogo luogo, Date data, Integer giorniPromemoria, Integer numeroPartecipanti, Integer durataInOre) { this(argomento, luogo, data); this.giorniPromemoria = giorniPromemoria; this.numeroPartecipanti = numeroPartecipanti; this.durataInOre = durataInOre; } public Seminario(String argomento, Luogo luogo, Date data, Integer giorniPromemoria, Integer numeroPartecipanti, Integer durataInOre, Integer numeroMassimoPartecipanti) { this(argomento, luogo, data, giorniPromemoria,numeroPartecipanti, durataInOre); this.numeroMassimoPartecipanti = numeroMassimoPartecipanti; } public String toString() { return "Il seminario è intitolato '" + argomento + "" + data != null ? "' si tiene in data " + dt.format(data) : ""; } }
Il codice presentato è il plausibile risultato di veloci rimaneggiamenti causati dall’aggiunta di informazioni non previste dall’analisi iniziale del progetto. Si può pensare che, per ogni change request, sia stato aggiunto un nuovo costruttore incrementando il costruttore più “esteso” con dei parametri opzionali; questa tecnica viene definita telescoping. Osservando il codice, si può dedurre che l’unico parametro obbligatorio per poter definire un seminario sia l’argomento.
Al fine di non complicare la classe oltre lo scopo di questo post, è stato implementato solo un “grezzo” metodo toString
. Interessa, infatti, solo ragionare sulla costruzione dell’oggetto e non sulle logiche che esso implementa.
Una prima analisi porta a pensare che avere costruttori in più, e con molti parametri opzionali, assicuri più flessibilità al codice chiamante. Quello che si ottiene, invece, è un codice poco leggibile.
Calendar gc = GregorianCalendar.getInstance(); gc.set(2011, Calendar.OCTOBER, 1); Date primoOttobre = gc.getTime(); Seminario bestPractice = new Seminario("Java best practice", Seminario.Luogo.SEDE, primoOttobre, 10, 2, 3, 3);
E’ difficile dire, a colpo d’occhio, cosa vogliano dire i quattro numeri utilizzati nel costruttore a meno di andare a consultare il javadoc (se c’è) o direttamente il codice sorgente della classe, con conseguente perdita di tempo.
Un’alternativa può essere quella di trasformare Seminario
in un JavaBean con un construttore senza parametri e un setter per ciascun parametro obbligatorio o opzionale, lasciando massima flessibilità di costruzione al codice client. Nulla però assicura che il bean venga utilizzato quando tutti i parametri obbligatori sono stati settati o quando lo stato interno del bean è coerente, cosa che invece si può fare facilmente utilizzando un costruttore parametrico.
Fatte queste considerazioni, è presentata l’applicazione del Builder Pattern al caso di esempio, con l’introduzione della classe SeminarioBuilder
che si occuperà della costruzione dell’oggetto di base Seminario
.
package it.cosenonjaviste.builderpattern.patternstep1; import it.cosenonjaviste.builderpattern.patternstep1.Seminario.Luogo; import java.util.Date; public class SeminarioBuilder { private String argomento; private Luogo luogo; // obbligatorio private Date data; // obbligatorio private Integer numeroPartecipanti; private Integer giorniPromemoria; private Integer durataInOre; private Integer numeroMassimoPartecipanti; public void setLuogo(Luogo luogo) { this.luogo = luogo; } public void setData(Date data) { this.data = data; } public void setNumeroPartecipanti(Integer numeroPartecipanti) { this.numeroPartecipanti = numeroPartecipanti; } public void setGiorniPromemoria(Integer giorniPromemoria) { this.giorniPromemoria = giorniPromemoria; } public void setDurataInOre(Integer durataInOre) { this.durataInOre = durataInOre; } public void setNumeroMassimoPartecipanti(Integer numeroMassimoPartecipanti) { this.numeroMassimoPartecipanti = numeroMassimoPartecipanti; } public SeminarioBuilder(String argomento) { this.argomento = argomento; } public Seminario build() { Seminario s = new Seminario(); s.argomento = argomento; s.luogo = this.luogo; s.data = this.data; s.numeroPartecipanti = this.numeroPartecipanti; s.durataInOre = this.durataInOre; s.numeroMassimoPartecipanti = this.numeroMassimoPartecipanti; s.giorniPromemoria = this.giorniPromemoria; return s; } }
L’implementazione di SeminarioBuilder
prevede le stesse member variable di Seminario
e un solo costruttore che riporta tutti, e soltanto, i parametri obbligatori della classe che si vuole costruire. Per ogni parametro opzionale, è aggiunto un setter.
Il metodo più importante è chiaramente build
che si occupa di istanziare Seminario
e di assegnare ad ogni sua variabile il valore memorizzato nel builder.
Seminario
a questo punto riporta solo il codice necessario al suo funzionamento mentre quello relativo alla costruzione è demandato al builder. Entrambe le classi sono nello stesso package e le variabili membro di Seminario
sono accessibili al builder poiché visibili a livello di package (questa condizione non è necessaria per applicare il pattern e sarà rimossa in seguito).
package it.cosenonjaviste.builderpattern.patternstep1; import java.text.DateFormat; import java.util.Date; public class Seminario { public enum Luogo { SEDE, CLIENTE } private static DateFormat dt = DateFormat.getDateInstance(); String argomento; // obbligatorio Luogo luogo; Date data; Integer numeroPartecipanti; Integer giorniPromemoria; Integer durataInOre; Integer numeroMassimoPartecipanti; public String toString() { return "Il seminario è intitolato '" + argomento + "" + data != null ? "' si tiene in data " + dt.format(data) : ""; } }
Il codice client è quindi modificato come segue.
SeminarioBuilder bestPracticeBuilder = new SeminarioBuilder("Java best practice"); bestPracticeBuilder.setLuogo(Seminario.Luogo.SEDE); bestPracticeBuilder.setData(primoOttobre); bestPracticeBuilder.setGiorniPromemoria(10); bestPracticeBuilder.setNumeroPartecipanti(2); bestPracticeBuilder.setDurataInOre(3); bestPracticeBuilder.setNumeroMassimoPartecipanti(3); Seminario bestPractice = bestPracticeBuilder.build();
L’istanziazione della classe è ora è più leggibile e consente di comprendere velocemente quali parametri siano passati per la costruzione.
La validazione dell’oggetto può essere implementata nel metodo build
prima della creazione di Seminario
; dato che questo metodo è l’unico punto della classe preposto alla creazione dell’oggetto, si garantisce di confinare tutto il codice di validazione migliorandone la manutenibilità. Nell’esempio del seminario, il numero di partecipanti non può eccedere il numero massimo di posti disponibili, quindi la validazione di questa regola può essere implementata in build
.
public Seminario build() { if (numeroPartecipanti != null && numeroMassimoPartecipanti != null){ if (numeroPartecipanti > numeroMassimoPartecipanti) throw new IllegalStateException("Il numero di partecipanti eccede lo spazio disponibile"); } Seminario s = new Seminario(); s.argomento = argomento; s.luogo = this.luogo; s.data = this.data; s.numeroPartecipanti = this.numeroPartecipanti; s.durataInOre = this.durataInOre; s.numeroMassimoPartecipanti = this.numeroMassimoPartecipanti; s.giorniPromemoria = this.giorniPromemoria; return s; }
Un altro vantaggio di questo pattern è la possibilità di istanziare nuovi oggetti simili a quello appena creato, minimizzando il codice da scrivere. Si supponga di voler istanziare un seminario in una data diversa ma con le stesse caratteristiche del seminario precedente. Senza utilizzare un nuovo builder, è sufficiente chiamare ancorabuild
dopo aver modificato i parametri del nuovo oggetto, come dimostrato dal seguente codice.
gc.set(Calendar.MONTH, Calendar.DECEMBER); gc.set(Calendar.DAY_OF_MONTH,12); Date primoDicembre = gc.getTime(); bestPracticeBuilder.setData(primoDicembre); Seminario bestPractice2ndEdition = bestPracticeBuilder.build();
Migliorare il builder
Parametri correlati
Alcuni accorgimenti consentono di migliorare SeminarioBuilder
. Qualora ci fossero due o più parametri opzionali correlati tra loro, è possibile sostituire i setter del builder con un unico metodo che eventualmente valida i parametri immediatamente. Utilizzando la regola di validazione precedente, è introdotto il metodo setPartecipanti
public void setPartecipanti(Integer numeroMassimoPartecipanti, Integer numeroPartecipanti) { if (numeroPartecipanti != null && numeroMassimoPartecipanti != null) { if (numeroPartecipanti > numeroMassimoPartecipanti) throw new IllegalArgumentException("Il numero di partecipanti eccede lo spazio disponibile"); } this.numeroMassimoPartecipanti = numeroMassimoPartecipanti; this.numeroPartecipanti = numeroPartecipanti; }
Rendere compatto il codice chiamante
Un altro miglioramento è raggiungibile modificando ogni setter del builder, in modo che al posto di void ritorni il builder stesso. Ad esempio, il metodo setDurataInOre
è modificato come segue.
public SeminarioBuilder setDurataInOre(Integer durataInOre) { this.durataInOre = durataInOre; return this; }
In questo modo, il codice client guadagna compattezza.
SeminarioBuilder bestPracticeBuilder = new SeminarioBuilder("Java best practice") .setLuogo(Seminario.Luogo.SEDE) .setData(primoOttobre) .setGiorniPromemoria(10) .setPartecipanti(2,3) .setDurataInOre(3);
Questo tipo di tecnica, che potrebbe sembrare inusuale a prima vista, in realtà è usata anche nel JRE, ad esempio dalle classi che implementano l’interfaccia java.lang.Appendable
come StringBuilder
o OutputStreamWriter
. Nel caso dello StringBuilder
, il metodo append
permette di “parametrizzare” la stringa, mentre il metodo toString
svolge la funzione del metodo build
.
String s = new StringBuilder("www.").append("cosenonjaviste.").append("it").toString();
ll builder come inner class
Non è necessario che SeminarioBuilder
sia una classe autonoma rispetto a Seminario
data la stretta relazione esistente. Nel codice sottostante, SeminarioBuilder
è stato riformulato (e rinominato Builder
) come static inner class di Seminario
. Per evitare confusione con le member variable di Seminario
, al nome delle variabili di Builder
è stato anteposto un underscore.
package it.cosenonjaviste.builderpattern.patternstep3; import java.text.DateFormat; import java.util.Date; public class Seminario { public enum Luogo { SEDE, CLIENTE } private static DateFormat dt = DateFormat.getDateInstance(); private String argomento; // obbligatorio private Luogo luogo; private Date data; private Integer numeroPartecipanti; private Integer giorniPromemoria; private Integer durataInOre; private Integer numeroMassimoPartecipanti; public String toString() { return "Il seminario intitolato '" + argomento + "' si tiene in data " + dt.format(data) + " presso " + luogo; } public static class Builder { private String _argomento; // obbligatorio private Luogo _luogo; private Date _data; private Integer _numeroPartecipanti; private Integer _giorniPromemoria; private Integer _durataInOre; private Integer _numeroMassimoPartecipanti; public Builder setLuogo(Luogo luogo) { _luogo = luogo; return this; } public Builder setData(Date data) { _data = data; return this; } public Builder setNumeroPartecipanti(Integer numeroPartecipanti) { _numeroPartecipanti = numeroPartecipanti; return this; } public Builder setGiorniPromemoria(Integer giorniPromemoria) { _giorniPromemoria = giorniPromemoria; return this; } public Builder setDurataInOre(Integer durataInOre) { _durataInOre = durataInOre; return this; } public Builder setNumeroMassimoPartecipanti(Integer numeroMassimoPartecipanti) { _numeroMassimoPartecipanti = numeroMassimoPartecipanti; return this; } public Builder(String argomento) { _argomento = argomento; } public Seminario build() { return new Seminario(this); } } private Seminario(){} public Seminario(Builder builder) { argomento = builder._argomento; luogo = builder._luogo; data = builder._data; numeroPartecipanti = builder._numeroPartecipanti; durataInOre = builder._durataInOre; numeroMassimoPartecipanti = builder._numeroMassimoPartecipanti; giorniPromemoria = builder._giorniPromemoria; if (numeroPartecipanti != null && numeroMassimoPartecipanti != null) { if (numeroPartecipanti > numeroMassimoPartecipanti) throw new IllegalStateException("Il numero di partecipanti eccede lo spazio disponibile"); } } }
Questo consente al builder di essere invocato direttamente dalla classe che costruisce e di accedere alle variabili membro della classe anche se private (rimuovendo la visibilità a livello di package assunta nella prima parte).
Seminario bestPractice = new Seminario.Builder("Java best practice") .setLuogo(Seminario.Luogo.SEDE) .setData(primoOttobre) .setGiorniPromemoria(10) .setNumeroPartecipanti(2) .setDurataInOre(3) .setNumeroMassimoPartecipanti(3).build();
Conclusioni
Il Builder Pattern è una valida alternativa all’overloading dei costruttori. L’applicazione di questo pattern richiede la scrittura di una classe in più rispetto quella di partenza, quindi non è sempre pratico applicarlo quando il numero di parametri della classe base è basso, oppure non esistono parametri opzionali. Tuttavia, potrebbe essere conveniente introdurlo da subito, qualora si supponga che il numero di parametri opzionali possa crescere in futuro. Non richiede, infatti, la riscrittura del codice client che non utilizza i nuovi parametri e non fa proliferare i costruttori secondo il telescoping.
Un vantaggio intrinseco è, inoltre, quello di ottenere un codice chiamante più facile da leggere e da scrivere. Il tempo investito nella preparazione del builder si ripaga, infatti, velocizzando la comprensione del codice client (che molto probabilmente capita di leggere diverse volte).
Infine, l’approccio con il builder permette di centralizzare la validazione della classe base in un unico metodo (con conseguente maggior facilità di manutenzione) e di essere facilmente impiegabile per restituire oggetti immutabili.
Pingback: ()
Pingback: ()