il problema dei costruttori
Quando una classe in Java comincia ad avere molti campi, la creazione di un oggetto tramite costruttore diventa un qualcosa di infernale per il programmatore. Facciamo un esempio per chiarirci le idee:
public class Animal { public enum Sex{ MALE, FEMALE } /* pedigreeCode campo obbligatori e non modifica */ private final String id; private String name; private String pedigreeName; private String owner; private String race; private String residence; private Boolean isVaccinated; private Boolean isChampion; private List sons; private Sex sex; private Double weight; private Double height; //getter e setter public Animal(String name, String pedigreeName, String id, String owner, String race, String residence, Boolean isVaccinated, Boolean isChampion, List sons, Sex sex, Double weight, Double height) { this.name = name; this.pedigreeName = pedigreeName; this.id = id; this.owner = owner; this.race = race; this.residence = residence; this.isVaccinated = isVaccinated; this.isChampion = isChampion; this.sons = sons; this.sex = sex; this.weight = weight; this.height = height; } public Animal(String id,String name, String pedigreeName) { this.name = name; this.pedigreeName = pedigreeName; this.id = id; } public Animal(String id, String owner, String race, String residence) { this.id = id; this.owner = owner; this.race = race; this.residence = residence; } public Animal(String id){ this.id = id; } }
Inanzitutto notiamo che se alcuni campi sono opzionali nella creazione di un oggetto le alternative sono due:
- Creare un unico costruttore con tutti i campi e passare null come valore per i campi non usati.
- Creare diverse versioni di costruttori con un differente set di campi da specificare. Soluzione con meno margine di errore nell’utilizzo ma più costosa per il programmatore in fase di sviluppo di una classe.
In entrambe le soluzioni la probabilità di errore è altissima.
Animal pluto = new Animal("pluto", "123", "PlutoSecondo", "labrador","Marco Rossi","Via x",true, false, null, Animal.Sex.MALE,40.5,30.0);
Nell’esempio precedente abbiamo specificato labrador come owner e Marco Rossi come razza! Effettivamente, utilizzando il costruttore per posizione dei parametri, fare un errore è facilissimo, per non parlare in fase di lettura del codice: è difficilissimo dire, a colpo d’occhio, cosa vogliano dire i dodici parametri utilizzati nel costruttore in fase di instansazione di un oggetto, a meno di andare a consultare il javadoc (se presente) o direttamente il codice sorgente della classe, con conseguente perdita di tempo.
Un’alternativa può essere quella di trasformare la classe Animal in un JavaBean con un construttore senza parametri e un setter per ciascun parametro obbligatorio o opzionale. Inoltre nulla assicura che il bean venga utilizzato quando tutti i parametri obbligatori sono stati settati o quando lo stato interno del bean è coerente.
Questo articolo pone una possibile soluzione molto utile da utilizzare, in alternativa al costruttore, quando una classe ha molti parametri: il builder pattern.
builder pattern – teoria
Il builder pattern è uno dei più importanti pattern, meglio conosciuti come GoF design pattern (per i novelli del mestiere i Gof sono gli autori del libro Design Patterns: Elements of Reusable Object-Oriented Software … se non avete letto questo libro correte subito a comprarlo…la bibbia del programmatore!).
Nella programmazione ad oggetti tale pattern è molto di voga poiché separa la costruzione di un oggetto complesso dalla sua rappresentazione, cosicché il processo di costruzione stesso possa creare diverse rappresentazioni. In questo modo l’algoritmo per la creazione di un oggetto complesso è indipendente dalle varie parti che costituiscono l’oggetto e da come vengono assemblate. Ciò ha l’effetto immediato di rendere più semplice la classe, permettendo a una classe builder separata di focalizzarsi sulla corretta costruzione di un’istanza e lasciando che la classe originale si concentri sul funzionamento degli oggetti. Questo è particolarmente utile quando volete assicurarvi che un oggetto sia valido prima di istanziarlo, e non volete che la logica di controllo appaia nei costruttori degli oggetti. Un builder permette anche di costruire un oggetto passo-passo, cosa che si può verificare quando si fa il parsing di un testo o si ottengono i parametri da un’interfaccia interattiva. Per maggiori dettagli e spiegazioni dal punto di vista formale e teorico vi rimando alla lettura di Design Patterns: Elements of Reusable Object-Oriented Software.
builder pattern – esempio
Proviamo ad implementare il builder pattern sulla classe Animal mostrata ad inizio articolo.
public final class AnimalBuilder { private String id; private String name; private String pedigreeName; private String owner; private String race; private String residence; private Boolean isVaccinated; private Boolean isChampion; private List<String> sons; private Animal.Sex sex; private Double weight; private Double height; private AnimalBuilder(String id) { this.id = id; } public static AnimalBuilder newBuilder(String id) { return new AnimalBuilder(id); } public AnimalBuilder name(String name) { this.name = name; return this; } public AnimalBuilder pedigreeName(String pedigreeName) { this.pedigreeName = pedigreeName; return this; } public AnimalBuilder owner(String owner) { this.owner = owner; return this; } public AnimalBuilder race(String race) { this.race = race; return this; } public AnimalBuilder residence(String residence) { this.residence = residence; return this; } public AnimalBuilder isVaccinated(Boolean isVaccinated) { this.isVaccinated = isVaccinated; return this; } public AnimalBuilder isChampion(Boolean isChampion) { this.isChampion = isChampion; return this; } public AnimalBuilder sons(List<String> sons) { this.sons = sons; return this; } public AnimalBuilder sex(Animal.Sex sex) { this.sex = sex; return this; } public AnimalBuilder weight(Double weight) { this.weight = weight; return this; } public AnimalBuilder height(Double height) { this.height = height; return this; } public Animal build() { return new Animal(name, pedigreeName, id, owner, race, residence, isVaccinated, isChampion, sons, sex, weight, height); } }
Adesso un oggetto di tipo Animal può essere istanziato anche nel seguente modo:
Animal pluto2 = AnimalBuilder.newBuilder("0000001") .name("0000001") .pedigreeName("PlutoSecondo") .owner("Marco Rossi") .race("labrador") .residence("Via x") .isVaccinated(true) .isChampion(false) .sons(null) .sex(Animal.Sex.MALE) .weight(40.5) .height(30.0) .build();
Notiamo subito che la creazione dell’oggetto tramite il Builder risulta essere nettamente più leggibile. Non viene passato il valore delle proprietà posizionalmente, ma con l’apposito metodo del Builder, evitando equivoci ed errori nel passaggio dei valori. Il risultato è quello di ottenere un codice chiamante più facile da leggere e da scrivere.
Un altro vantaggio di questo pattern è la possibilità di istanziare oggetti cloni o simili a quello appena creato, minimizzando il codice da scrivere. Supponiamo di voler instanziare due animali identici e un terzo animale identico, ma di sesso opposto. Senza utilizzare un nuovo builder, è sufficiente chiamare ancora build
dopo aver modificato i parametri del nuovo oggetto, come dimostrato dal seguente codice:
AnimalBuilder animalBuilder = AnimalBuilder.newBuilder("0000001") .name("0000001") .pedigreeName("PlutoSecondo") .owner("Marco Rossi") .race("labrador") .residence("Via x") .isVaccinated(true) .isChampion(false) .sons(null) .sex(Animal.Sex.MALE) .weight(40.5) .height(30.0); Animal animal3A = animalBuilder.build(); Animal animal3AClone = animalBuilder.build(); Animal animal3B = animalBuilder.sex(Animal.Sex.FEMALE).build();
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. Applichiamo questo principio nel nostro esempio inserendo i check di validazione all’interno del metodo build
prima della creazione di Animal
; dato che questo metodo è l’unico punto della classe adibito alla creazione dell’oggetto:
public Animal build() { if(weight>200){ throw new IllegalArgumentException("Animale troppo pesante"); } if(!isVaccinated){ throw new IllegalArgumentException("Animale non vaccinato"); } return new Animal(name, pedigreeName, id, owner, race, residence, isVaccinated, isChampion, sons, sex, weight, height); }
builder come inner class
Non è strettamente necessario che la Builder class sia una classe definita in un file a parte. È possibile definire la builder class come static inner class della classe di definizione dell’oggetto. Trovo personalmente più confusionario questo approccio, pertanto non lo seguirò negli esempi che vi sto proponendo.
svantaggi del builder
Parliamo adesso dei svantaggi nell’utilizzare il builder nei propri progetti. L’unico reale svantaggio è quello di dover definire una builder class per classe con conseguente incremento del tempo di sviluppo. A mio avviso il tempo investito nella preparazione del builder viene sempre ripagato, velocizzando la comprensione del codice client e la manutenzione. Esistono però possibili soluzioni per automatizzare la creazione delle builder class. Riporto le tecniche più utilizzate oggi.
BUILDER IDE PLUGIN
Gli IDE più utilizzati oggi (ad es. Eclipse, Intellij, ecc) offrono dei fantastici plugin per automatizzare la creazione di Builder Class. Io personalmente utilizzo Builder Generator, facilmente scaricabile dal menu Plugins di Intellij. Accedendo agli shortcut di Intellij con ALT+INS puoi autogenerare in un secondo la classe di Builder della classe corrente scegliendo:
- nome della classe
- prefisso del metodo: io lascio vuoto di solito questo parametro per creare i metodi del builder con il medesimo nome
- package di destinazione
- inner builder: se spuntata questa opzione il plugin creerà il builder all’interno della classe corrente come inner builder class.
BUILDER con lombok
Un altra ottima alternativa per automatizzare la creazione del builder pattern con Lombok. Per sapere di più su cos’è Lombok leggetevi questo esaustivo nostro articolo: LINK.
Utilizzando la seguente annotation sopra la classe
@Builder(builderMethodName = "newBuilder") public class AnimalLombok {
Lombok genererà in fase di build la builder class. Lato programmatore non ci sarà nessun side-effect: riuscirete ad utilizzare il builder method senza aver scritto una riga di codice!
AnimalLombok plutoLombok = AnimalLombok.newBuilder() .id("0000001") .name("0000001") .pedigreeName("PlutoSecondo") .owner("Marco Rossi") .race("labrador") .residence("Via x") .isVaccinated(true) .isChampion(false) .sons(null) .sex(Animal.Sex.MALE) .weight(40.5) .height(30.0) .build();
conclusioni
Vi consiglio altamente di utilizzare il builder pattern nei grossi progetti per i motivi che ho citato in questo articolo: io non posso più farne a meno!
Trovate gli esempi di questo articolo sul mio GITHUB
bibliografia e riferimenti
- Design Patterns: Elements of Reusable Object-Oriented Software (Autori: Erich Gamma, John Vlissides, Ralph Johnson, Richard Helm)