Dai costruttori al Builder Pattern in Java

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.

Giampaolo Trapasso

Sono laureato in Informatica e attualmente lavoro come Software Engineer in Databiz Srl. Mi diverto a programmare usando Java e Scala, Akka, RxJava e Cassandra. Qui mio modesto contributo su StackOverflow e il mio account su GitHub

  • Pingback: Sviluppo di applicazioni Android con AndroidAnnotations | Cose Non Javiste()

  • Pingback: Gson, da Java a JSON e viceversa: alcuni casi "difficili" :: CoseNonJaviste()

  • Marco

    Ciao Giampaolo, è sempre attualissimo questo argomento, quante volte si fa un uso sconsiderato dei costruttori. Usando un builder si rende veramente più pulito e leggibile tutto il codice. W il builder!

    • Giampaolo Trapasso

      Ciao Marco, sei andato a pescare il mio primo post su CNJ.. è passato un po’ di tempo 🙂

  • ozeta86

    ciao Giampaolo, a 5 anni di distanza dalla sua pubblicazione posso dire che ho trovato il post ( e questo blog!) davvero utilissimo! 🙂

  • Junaid Muhammad

    Ciao Giampaolo, ho trovato il tuo articolo veramente utile. Vorrei il tuo commento riguardo due punti:
    primo, perché dovrei usare package private access modifier per gli attributi della mia classe?(il che violerebbe il principio di incapsulazione).
    secondo, utilizzando la classe builder uno si accorge degli eventuali errori soltanto nel momento in cui viene chiamato il metodo build.
    Grazie!

    • Giampaolo Trapasso

      Ciao!
      Se parliamo della classe Seminario, non ho messo i getter per non rendere la classe lunga da leggere, mi sono accontentato solo del metodo toString. Per quanto riguarda la seconda nota, la cosa è voluta proprio nel pattern. Puoi avere uno stato “incompleto” durante i diversi passi ed effettuare la validazione di quello che hai inserito proprio nel metodo build. Aggiungo anche che avendo un linguaggio che permette di aggiungere un default ai parametri del costruttore (chi ha detto Scala? 😉 ), in parte l’utilizzo di questo pattern viene ad essere ridimensionato. Se ne vuoi sapere di più, raggiungici su https://slackin-codingjam.herokuapp.com/ Alla prossima!

  • ChrisXXV

    Ottimo articolo lo metto tra la mia lista di implementazioni “evergreen” 🙂
    Come vedi come alternativa un costruttore che accetta, come parametro, una classe di parametri
    che si può ampliare?

    • Giampaolo Trapasso

      Ciao ChrisXXV e grazie 🙂
      Se ho ben capito, tu vorresti usare in alternativa ad una lista di parametri un’unica classe come parametro del costruttore per la tua classe “target” e poi andare a cambiare solo questa classe parametro.

      I miei pensieri su questo. Se scopri che i tuoi parametri in qualche maniera tra di loro sono coesi, penso che vada bene. Supponiamo ad esempio che una classe VisitaMedica prenda come parametro del costruttore una data di inizio e una data di fine. In questo caso potrebbe aver senso introdurre una nuova classe Intervallo che inglobi le due date e passare solo Intervallo a VisitaMedica.

      Ma cosa succede se voglio aggiungere anche nome e cognome del dottore al costruttore di VisitaMedica? Potrei aggiungere una classe Dottore come parametro di VisitaMedica. Ora, ha senso avere una classe che inglobi tutti e due (o quattro) i parametri? Come la chiameresti? ParametriVisitaMedica probabilmente con due parametri Intervallo e Dottore. Che scopo avrebbe mettere questi due parametri assieme? Probabilmente nessuno al di fuori del costruttore di VisitaMedica. Avere ParametriVisitaMedicaParam ti sposta solo il problema dal costruttore di VisitaMedica al quello di ParametriVisitaMedica senza, secondo me, nessun beneficio.

      Cosa fa invece il builder? Il builder in pratica è una forma di DSL. Un modo per aiutare il programmatore che usa il tuo costruttore (e questo programmatore potresti essere anche tu 5 giorni dopo aver scritto la tua classe) a costruire correttamente un oggetto con parametri opzionali e non. Volendo, con un po’ di codice, si può fare qualcosa stile “wizard” cioè con un oggetto che può essere costruito in più passi, controlla ad esempio https://www.javacodegeeks.com/2012/01/wizard-design-pattern.html.

      Penso di aver scritto il commento più lungo nella storia di questo blog ;), ma se vuoi fare altre domande o semplicemente ti va di fare 4 chiacchiere, raggiungici su https://slackin-codingjam.herokuapp.com/

      • ChrisXXV

        Interessante, grazie delle info