Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype
In questa pagina 7
Definizione. I pattern creazionali riguardano l'inizializzazione e la configurazione di classi e oggetti: nascondono quali classi concrete si creano e come, così che il resto del programma dipenda solo da interfacce (Design pattern - introduzione e catalogo GoFUn design pattern è una soluzione generale, riusabile e collaudata a un problema ricorrente di progettazione in un certo contesto, con un nome (che arricchisce il vocabolario dei progettisti), un problema, una soluzione (struttura e partecipanti) e le conseguenze; il catalogo GoF (Gamma, Helm, Johnson, Vlissides) ha 23 pattern in tre famiglie: 5 creazionali (come si creano gli oggetti), 7 strutturali (come si compongono classi e oggetti), 11 comportamentali (come collaborano e si ripartiscono le responsabilità); dietro i pattern ci sono principi come programmare verso un'interfaccia, incapsulare ciò che varia, preferire la composizione all'ereditarietà, aperto/chiuso e inversione delle dipendenze; un pattern si sceglie dal problema, non dal nome, e usarlo senza bisogno complica il progetto.Design pattern - introduzione e catalogo GoF →). Sono cinque: Singleton, Factory Method, Abstract Factory, Builder, Prototype.
Esempi sul dominio biblioteca; ogni programma è stato compilato ed eseguito con JDK 21. Il problema comune è quello di new: un new Libro(...) dentro la logica la lega alla classe concreta, e un if che sceglie tra new Libro, new Dvd, new Rivista dipende da tutte le classi e va modificato a ogni nuovo tipo.
| pattern | domanda a cui risponde |
|---|---|
| Singleton | come garantisco una sola istanza? |
| Factory Method | come lascio alle sottoclassi la scelta della classe da creare? |
| Abstract Factory | come creo oggetti che devono andare insieme, senza nominare le classi concrete? |
| Builder | come costruisco un oggetto con tanti campi, in parte facoltativi? |
| Prototype | come creo oggetti copiando uno già configurato? |
Singleton
Intento. Assicurare che una classe abbia una sola istanza e fornire un punto di accesso globale ad essa.
Quando. Una risorsa unica per natura: configurazione, gestore di un dispositivo (lo spooler di stampa), registro, pool di connessioni. Rispetto a una variabile static, il Singleton dichiara l'intenzione di unicità e permette l'inizializzazione lazy (all'uso) invece di quella eager (all'avvio).
| ruolo | classe | responsabilità |
|---|---|---|
| Singleton | la classe stessa | costruttore privato; campo statico con l'istanza; metodo statico pubblico getInstance() che la restituisce (creandola alla prima chiamata, se lazy) |
| client | chiunque | chiama Singleton.getInstance(), non usa mai new |
Esempio del corso (Singleton con getInstance, add, getCount): vedi l'esercizio Esercizio - Singleton, thread e riflessione (esempi del corso). Lo schema è
private static Singleton myInstance = null;
private Singleton() { }
public static Singleton getInstance() {
if (myInstance == null) myInstance = new Singleton();
return myInstance;
}Il problema dei thread e le varianti
Con più thread la versione lazy ha una race condition: due thread vedono myInstance == null insieme e creano due istanze (il corso lo mostra con il bollitore di cioccolato, ChocolateBoiler, che comincia a malfunzionare dopo l'ottimizzazione multithread; vedi Concorrenza in Java - thread, sincronizzazione e poolIn Java un thread si crea con Thread e Runnable e si avvia con start() (run() chiamato direttamente non crea nessun thread); i thread che modificano dati condivisi producono race condition (aggiornamenti persi) se la sequenza lettura-modifica-scrittura non è atomica, e si protegge con synchronized (monitor dell'oggetto, rientrante), con classi atomiche o con lock; wait/notifyAll dentro un synchronized, sempre in un ciclo while, realizzano l'attesa di una condizione (pool di risorse, produttore-consumatore); l'ordine opposto di acquisizione di due lock provoca stallo (deadlock) e si evita con un ordine globale; i thread pool (ExecutorService, Callable, Future) riusano pochi thread per molti compiti; i pattern concorrenti ricorrenti sono monitor, produttore-consumatore, thread pool, Singleton thread-safe, oggetti immutabili, future.Concorrenza in Java - thread, sincronizzazione e pool →).
| variante | come | pro e contro |
|---|---|---|
| lazy ingenuo | if (istanza == null) istanza = new ... |
non thread-safe |
lazy con synchronized |
metodo static synchronized getInstance() |
corretto, ma ogni chiamata paga il lock (nel corso: "100 volte più lento") |
| eager | private static final X ISTANZA = new X(); |
corretto (il caricamento della classe è thread-safe), ma si crea anche se non serve |
| double-checked locking | doppio controllo, synchronized solo la prima volta, campo volatile |
lazy e veloce, ma volatile è indispensabile (senza, un thread può vedere un oggetto non inizializzato del tutto) |
| holder idiom | classe interna statica che contiene l'istanza | lazy, senza lock, semplice |
| enum | enum Config { ISTANZA; ... } |
thread-safe, immune alla riflessione e alla serializzazione, ma non si può ereditare |
Linee guida del corso: se le prestazioni non contano o c'è un solo thread, la versione semplice; se la creazione è economica, eager; se è costosa, double-checked locking.
Errori nel materiale del corso. (1) La slide sul lazy Singleton scrive
if (uniqueInstance == null) { return new ChocolateBoiler(); } else { return uniqueInstance; }: non assegna mai l'istanza al campo, quindi ogni chiamata crea un oggetto nuovo (provato:get() == get()dàfalse); anche la versionesynchronizeddella stessa slide ha l'errore. La dispensa corregge l'assegnamento. (2) Il double-checked locking della dispensa hareturn uniqueInstance;dentro l'if: quando l'istanza esiste, il metodo non restituisce niente e il compilatore dàmissing return statement. Ilreturnva fuori dall'if, come nel programma sotto. (3) NeltoStringdelSingletondel corso manca il>di chiusura.
import java.lang.reflect.Constructor;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/** 1) Lazy ingenuo: NON e' thread-safe (il bug delle slide: due thread possono creare due istanze). */
class ConfigIngenua {
static final AtomicInteger create = new AtomicInteger();
private static ConfigIngenua istanza;
private ConfigIngenua() {
create.incrementAndGet();
try { Thread.sleep(30); } catch (InterruptedException e) { } // costruzione "costosa"
}
static ConfigIngenua get() {
if (istanza == null) istanza = new ConfigIngenua();
return istanza;
}
}
/** 2) Lazy con metodo synchronized: corretto, ma ogni accesso paga il lucchetto. */
class ConfigSync {
private static ConfigSync istanza;
private ConfigSync() { }
static synchronized ConfigSync get() {
if (istanza == null) istanza = new ConfigSync();
return istanza;
}
}
/** 3) Eager: creata al caricamento della classe (la JVM garantisce il caricamento thread-safe). */
class ConfigEager {
private static final ConfigEager ISTANZA = new ConfigEager();
private ConfigEager() { }
static ConfigEager get() { return ISTANZA; }
}
/** 4) Double-checked locking: volatile e' indispensabile. */
class ConfigDCL {
static final AtomicInteger create = new AtomicInteger();
private static volatile ConfigDCL istanza;
private ConfigDCL() {
create.incrementAndGet();
try { Thread.sleep(30); } catch (InterruptedException e) { }
}
static ConfigDCL get() {
ConfigDCL r = istanza; // una sola lettura del campo volatile
if (r == null) {
synchronized (ConfigDCL.class) {
r = istanza;
if (r == null) istanza = r = new ConfigDCL();
}
}
return r; // la versione delle slide dimentica questo return fuori dall'if
}
}
/** 5) Holder idiom: lazy, senza lucchetti, perche' la classe interna si carica alla prima get(). */
class ConfigHolder {
private ConfigHolder() { }
private static class Contenitore { static final ConfigHolder ISTANZA = new ConfigHolder(); }
static ConfigHolder get() { return Contenitore.ISTANZA; }
}
/** 6) Enum: unica, thread-safe, e immune a riflessione e serializzazione. */
enum ConfigEnum {
ISTANZA;
int prestitiMassimi = 5;
}
public class SingletonVarianti {
/** Lancia n thread che chiamano get() "insieme" e conta quante istanze sono nate. */
static void provaConcorrente(String nome, Runnable get, AtomicInteger create) throws InterruptedException {
CountDownLatch via = new CountDownLatch(1);
Thread[] t = new Thread[8];
for (int i = 0; i < t.length; i++) {
t[i] = new Thread(() -> {
try { via.await(); } catch (InterruptedException e) { }
get.run();
});
t[i].start();
}
via.countDown(); // tutti partono insieme
for (Thread x : t) x.join();
System.out.println(nome + ": istanze create = " + create.get());
}
public static void main(String[] args) throws Exception {
provaConcorrente("lazy ingenuo", ConfigIngenua::get, ConfigIngenua.create);
provaConcorrente("double-checked", ConfigDCL::get, ConfigDCL.create);
System.out.println("sync stessa istanza? " + (ConfigSync.get() == ConfigSync.get()));
System.out.println("eager stessa istanza? " + (ConfigEager.get() == ConfigEager.get()));
System.out.println("holder stessa istanza? " + (ConfigHolder.get() == ConfigHolder.get()));
// La riflessione rompe il costruttore privato (vedi la nota su Java reflection)...
Constructor<ConfigHolder> k = ConfigHolder.class.getDeclaredConstructor();
k.setAccessible(true);
ConfigHolder bis = k.newInstance();
System.out.println("holder dopo riflessione: ancora unica? " + (bis == ConfigHolder.get()));
// ...ma non quello di un enum
try {
Constructor<ConfigEnum> ke = ConfigEnum.class.getDeclaredConstructor(String.class, int.class);
ke.setAccessible(true);
ke.newInstance("COPIA", 1);
} catch (Exception e) {
System.out.println("enum: " + e);
}
}
}Uscita (il numero di istanze della versione ingenua, fino a 8, varia a ogni esecuzione: la costruzione "costosa" simulata con sleep rende la corsa quasi certa):
lazy ingenuo: istanze create = 8
double-checked: istanze create = 1
sync stessa istanza? true
eager stessa istanza? true
holder stessa istanza? true
holder dopo riflessione: ancora unica? false
enum: java.lang.IllegalArgumentException: Cannot reflectively create enum objectsLa riflessione rompe un Singleton con costruttore privato (setAccessible(true), vedi Java - generics, reflection e passaggio dei parametriIn Java ogni dato ha una propria identità a tempo di esecuzione (il cast è controllato e può lanciare ClassCastException, a differenza del C) e i parametri si passano sempre per valore (di un riferimento, per gli oggetti: si può modificare l'oggetto ma non riassegnare il riferimento del chiamante); i generics (CodaGenum no (Cannot reflectively create enum objects). Il Singleton non va usato come variabile globale comoda: crea dipendenze nascoste e rende difficili i test (non si può sostituire con un finto).
Simple Factory e Factory Method
Il passo zero (Simple Factory). Si sposta l'if di creazione in una classe a parte (SimplePizzaFactory nel corso, SimpleFactoryDocumenti sotto): il codice che usa i prodotti non dipende più dalle classi concrete. Non è un pattern GoF, e va modificata a ogni nuovo tipo.
Errore nel materiale del corso. Il metodo
createPizza(String type)della dispensa ha una catena diif/else ife nessunreturnfinale: non compila (missing return statement); nella classe astrattaPizzaStorela dichiarazioneprotected abstract createPizza(String type);non ha il tipo di ritorno (Pizza). Nel programma sotto sono corretti.
Intento (Factory Method). Definire un'interfaccia per creare un oggetto, lasciando alle sottoclassi la decisione di quale classe istanziare: una classe rinvia l'istanziazione alle sottoclassi.
Quando. Una classe non può prevedere la classe degli oggetti che deve creare; vuole che siano le sottoclassi a specificarli; vuole localizzare in un punto solo la scelta del prodotto.
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Product | Documento |
interfaccia degli oggetti creati |
| ConcreteProduct | Libro, Rivista, Dvd |
implementazioni |
| Creator | Biblioteca (astratta) |
contiene il codice comune acquisisci(...) e dichiara il factory method astratto creaDocumento(...) |
| ConcreteCreator | BibliotecaCivica, Mediateca |
implementano il factory method e scelgono la classe concreta |
Si crea una gerarchia parallela: ai ConcreteCreator corrispondono i ConcreteProduct. Il codice comune (acquisisci) dipende solo da Documento: è un piccolo framework (il Creator fornisce il servizio, il cliente fornisce la sottoclasse con il metodo di fabbrica). Realizza l'inversione delle dipendenze: Biblioteca dipende dall'astrazione Documento, e anche i prodotti concreti.
import java.util.ArrayList;
import java.util.List;
/** Prodotto: l'interfaccia che il client conosce. */
interface Documento {
String titolo();
int giorniPrestito();
}
class Libro implements Documento {
private final String titolo;
Libro(String titolo) { this.titolo = titolo; }
public String titolo() { return titolo; }
public int giorniPrestito() { return 30; }
@Override public String toString() { return "Libro(" + titolo + ")"; }
}
class Rivista implements Documento {
private final String titolo;
Rivista(String titolo) { this.titolo = titolo; }
public String titolo() { return titolo; }
public int giorniPrestito() { return 7; }
@Override public String toString() { return "Rivista(" + titolo + ")"; }
}
class Dvd implements Documento {
private final String titolo;
Dvd(String titolo) { this.titolo = titolo; }
public String titolo() { return titolo; }
public int giorniPrestito() { return 3; }
@Override public String toString() { return "Dvd(" + titolo + ")"; }
}
/** Simple Factory (non e' un pattern GoF): sposta l'if in una classe, ma va modificata a ogni nuovo tipo. */
class SimpleFactoryDocumenti {
static Documento crea(String tipo, String titolo) {
switch (tipo) {
case "libro": return new Libro(titolo);
case "rivista": return new Rivista(titolo);
case "dvd": return new Dvd(titolo);
default: throw new IllegalArgumentException("tipo sconosciuto: " + tipo);
}
}
}
/** Creator: contiene la parte comune (acquisisci) e DELEGA alle sottoclassi la scelta della classe concreta. */
abstract class Biblioteca {
private final List<Documento> catalogo = new ArrayList<>();
/** Il Factory Method: astratto, lo implementa ogni sottoclasse. */
protected abstract Documento creaDocumento(String tipo, String titolo);
/** Codice comune, scritto una volta sola e che dipende solo dall'interfaccia Documento. */
public Documento acquisisci(String tipo, String titolo) {
Documento d = creaDocumento(tipo, titolo);
catalogo.add(d);
System.out.println(getClass().getSimpleName() + " acquisisce " + d + ", prestito " + d.giorniPrestito() + " giorni");
return d;
}
public int dimensione() { return catalogo.size(); }
}
/** ConcreteCreator 1: biblioteca civica, solo libri e riviste. */
class BibliotecaCivica extends Biblioteca {
@Override protected Documento creaDocumento(String tipo, String titolo) {
switch (tipo) {
case "libro": return new Libro(titolo);
case "rivista": return new Rivista(titolo);
default: throw new IllegalArgumentException("la civica non acquisisce: " + tipo);
}
}
}
/** ConcreteCreator 2: mediateca, che aggiunge i DVD e allunga i prestiti dei libri. */
class Mediateca extends Biblioteca {
@Override protected Documento creaDocumento(String tipo, String titolo) {
if (tipo.equals("dvd")) return new Dvd(titolo);
if (tipo.equals("libro")) return new Libro(titolo) { // sottoclasse anonima: 45 giorni
@Override public int giorniPrestito() { return 45; }
};
throw new IllegalArgumentException("la mediateca non acquisisce: " + tipo);
}
}
public class FactoryMethod {
public static void main(String[] args) {
System.out.println(SimpleFactoryDocumenti.crea("dvd", "Metropolis"));
Biblioteca civica = new BibliotecaCivica();
civica.acquisisci("libro", "Il barone rampante");
civica.acquisisci("rivista", "Le Scienze");
Biblioteca media = new Mediateca(); // il client usa solo il tipo astratto
media.acquisisci("libro", "Il barone rampante");
media.acquisisci("dvd", "Metropolis");
try {
civica.acquisisci("dvd", "Metropolis");
} catch (IllegalArgumentException e) {
System.out.println("errore: " + e.getMessage());
}
}
}Dvd(Metropolis)
BibliotecaCivica acquisisce Libro(Il barone rampante), prestito 30 giorni
BibliotecaCivica acquisisce Rivista(Le Scienze), prestito 7 giorni
Mediateca acquisisce Libro(Il barone rampante), prestito 45 giorni
Mediateca acquisisce Dvd(Metropolis), prestito 3 giorni
errore: la civica non acquisisce: dvdVarianti. Factory method parametrizzato (come qui, con una stringa tipo, oppure con un enum); factory method con implementazione di default (non astratto: la sottoclasse può ridefinirlo); in Java i metodi statici di creazione (List.of, Integer.valueOf) sono "static factory method" senza ereditarietà, utili per dare nomi alle creazioni e restituire sottotipi.
Abstract Factory
Intento. Fornire un'interfaccia per creare famiglie di oggetti correlati o dipendenti senza specificarne le classi concrete.
Quando. Il sistema deve essere indipendente da come i prodotti sono creati e composti; deve essere configurato con una tra più famiglie di prodotti; i prodotti di una famiglia sono progettati per essere usati insieme e bisogna imporlo; si vuole una libreria di prodotti mostrando solo le interfacce. Esempio reale del corso: JDBC (Class.forName(driver) + DriverManager.getConnection(...): Connection, Statement sono interfacce, la famiglia concreta dipende dal driver).
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| AbstractFactory | FabbricaSupporto |
interfaccia con un metodo di creazione per ogni prodotto |
| ConcreteFactory | FabbricaCartacea, FabbricaDigitale |
creano i prodotti di una famiglia |
| AbstractProduct | Documento, Accesso |
interfaccia di ogni tipo di prodotto |
| ConcreteProduct | LibroCartaceo+SchedaScaffale; EBook+LicenzaDigitale |
prodotti di ciascuna famiglia |
| Client | Catalogatore |
usa solo le interfacce: non può mescolare cartaceo e digitale |
/** Prodotto A: il documento. */
interface Documento {
String descrizione();
}
/** Prodotto B: la scheda d'accesso che deve essere coerente con il tipo di documento. */
interface Accesso {
String istruzioni();
}
class LibroCartaceo implements Documento {
private final String titolo;
LibroCartaceo(String titolo) { this.titolo = titolo; }
public String descrizione() { return "libro cartaceo '" + titolo + "'"; }
}
class SchedaScaffale implements Accesso {
private final String segnatura;
SchedaScaffale(String segnatura) { this.segnatura = segnatura; }
public String istruzioni() { return "ritira al banco, scaffale " + segnatura; }
}
class EBook implements Documento {
private final String titolo;
EBook(String titolo) { this.titolo = titolo; }
public String descrizione() { return "e-book '" + titolo + "' (EPUB)"; }
}
class LicenzaDigitale implements Accesso {
private final String codice;
LicenzaDigitale(String codice) { this.codice = codice; }
public String istruzioni() { return "scarica con la licenza " + codice + " (scade in 14 giorni)"; }
}
/** AbstractFactory: una famiglia di prodotti correlati. */
interface FabbricaSupporto {
Documento creaDocumento(String titolo);
Accesso creaAccesso(String riferimento);
}
class FabbricaCartacea implements FabbricaSupporto {
public Documento creaDocumento(String titolo) { return new LibroCartaceo(titolo); }
public Accesso creaAccesso(String riferimento) { return new SchedaScaffale(riferimento); }
}
class FabbricaDigitale implements FabbricaSupporto {
public Documento creaDocumento(String titolo) { return new EBook(titolo); }
public Accesso creaAccesso(String riferimento) { return new LicenzaDigitale(riferimento); }
}
/** Client: non nomina mai una classe concreta, quindi non puo' mescolare cartaceo e digitale. */
class Catalogatore {
private final FabbricaSupporto fabbrica;
Catalogatore(FabbricaSupporto fabbrica) { this.fabbrica = fabbrica; }
String scheda(String titolo, String riferimento) {
Documento d = fabbrica.creaDocumento(titolo);
Accesso a = fabbrica.creaAccesso(riferimento);
return d.descrizione() + " -> " + a.istruzioni();
}
}
public class AbstractFactory {
public static void main(String[] args) {
// la scelta della famiglia avviene in un punto solo (qui: un argomento, di solito un file di configurazione)
String supporto = args.length > 0 ? args[0] : "tutti";
if (!supporto.equals("digitale")) {
System.out.println(new Catalogatore(new FabbricaCartacea()).scheda("Se questo e' un uomo", "B-12"));
}
if (!supporto.equals("cartaceo")) {
System.out.println(new Catalogatore(new FabbricaDigitale()).scheda("Se questo e' un uomo", "LIC-8841"));
}
}
}libro cartaceo 'Se questo e' un uomo' -> ritira al banco, scaffale B-12
e-book 'Se questo e' un uomo' (EPUB) -> scarica con la licenza LIC-8841 (scade in 14 giorni)Conseguenze. I nomi delle classi concrete stanno nelle fabbriche e non nel client; cambiare famiglia significa cambiare un solo punto (dove si istanzia la fabbrica); coerenza tra i prodotti; ma aggiungere un nuovo tipo di prodotto è difficile, perché l'interfaccia della fabbrica fissa l'insieme dei prodotti (va cambiata in tutte le fabbriche). Le fabbriche concrete sono spesso Singleton; ogni metodo di creazione è un Factory Method.
Factory Method e Abstract Factory. Il Factory Method si basa sull'ereditarietà (la creazione è nelle sottoclassi), l'Abstract Factory sulla composizione (la creazione è in oggetti-fabbrica passati al client). Nel corso, la pizzeria usa i due insieme: PizzaStore.createPizza è un factory method (la scelta dello stile regionale), PizzaIngredientFactory è l'abstract factory (gli ingredienti coerenti con la regione, così che i franchising non possano usare ingredienti scadenti).
Builder
Intento. Separare la costruzione di un oggetto complesso dalla sua rappresentazione, così da costruirlo passo per passo, con campi facoltativi e controlli sulle regole tra campi.
Quando. Un costruttore con molti parametri (soprattutto facoltativi) diventerebbe illeggibile (telescoping constructors: new Ricerca("rosa", null, 1980, null, true, null)); l'oggetto deve essere immutabile ma costruito in più passi; la costruzione deve validare l'insieme dei campi.
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Product | RichiestaRicerca |
oggetto finale, immutabile, costruttore privato che prende il builder |
| Builder | RichiestaRicerca.Builder |
campi obbligatori nel costruttore, metodi di impostazione che restituiscono this (interfaccia fluente), build() |
| Director (facoltativo) | il codice che chiama i metodi nell'ordine voluto | qui è il main |
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Oggetto complesso e immutabile, con campi obbligatori e facoltativi. */
final class RichiestaRicerca {
private final String parolaChiave; // obbligatoria
private final String autore; // facoltativi...
private final Integer annoDa, annoA;
private final boolean soloDisponibili;
private final List<String> lingue;
private RichiestaRicerca(Builder b) { // costruttore privato: si passa solo dal Builder
this.parolaChiave = b.parolaChiave;
this.autore = b.autore;
this.annoDa = b.annoDa;
this.annoA = b.annoA;
this.soloDisponibili = b.soloDisponibili;
this.lingue = Collections.unmodifiableList(new ArrayList<>(b.lingue));
}
@Override public String toString() {
return "Ricerca[" + parolaChiave + ", autore=" + autore + ", anni=" + annoDa + ".." + annoA
+ ", soloDisponibili=" + soloDisponibili + ", lingue=" + lingue + "]";
}
static class Builder {
private final String parolaChiave;
private String autore;
private Integer annoDa, annoA;
private boolean soloDisponibili = false;
private final List<String> lingue = new ArrayList<>();
Builder(String parolaChiave) {
if (parolaChiave == null || parolaChiave.isBlank())
throw new IllegalArgumentException("parola chiave obbligatoria");
this.parolaChiave = parolaChiave;
}
Builder autore(String a) { this.autore = a; return this; }
Builder dal(int anno) { this.annoDa = anno; return this; }
Builder al(int anno) { this.annoA = anno; return this; }
Builder soloDisponibili() { this.soloDisponibili = true; return this; }
Builder lingua(String l) { this.lingue.add(l); return this; }
RichiestaRicerca build() {
// le regole tra campi si controllano qui, una volta sola, prima di creare l'oggetto
if (annoDa != null && annoA != null && annoDa > annoA)
throw new IllegalStateException("intervallo di anni invertito: " + annoDa + " > " + annoA);
if (annoA != null && annoA > LocalDate.now().getYear())
throw new IllegalStateException("anno nel futuro: " + annoA);
return new RichiestaRicerca(this);
}
}
}
public class BuilderEx {
public static void main(String[] args) {
// leggibile: ogni valore ha il suo nome (con un costruttore a 6 parametri sarebbe new X("rosa", null, 1980, null, true, ...))
RichiestaRicerca r = new RichiestaRicerca.Builder("rosa")
.autore("Eco")
.dal(1980).al(1990)
.soloDisponibili()
.lingua("it").lingua("en")
.build();
System.out.println(r);
System.out.println(new RichiestaRicerca.Builder("fisica").build()); // solo il campo obbligatorio
try {
new RichiestaRicerca.Builder("rosa").dal(2000).al(1990).build();
} catch (IllegalStateException e) {
System.out.println("errore: " + e.getMessage());
}
}
}Ricerca[rosa, autore=Eco, anni=1980..1990, soloDisponibili=true, lingue=[it, en]]
Ricerca[fisica, autore=null, anni=null..null, soloDisponibili=false, lingue=[]]
errore: intervallo di anni invertito: 2000 > 1990Il builder controlla i vincoli tra campi in build() (intervallo di anni, anno futuro) e consente più rappresentazioni dello stesso procedimento. Nel GoF originale il Builder è un'interfaccia con più ConcreteBuilder e un Director che pilota la costruzione (esempio: convertire un documento in formati diversi); la forma "fluente" mostrata è la variante più comune in Java.
Prototype
Intento. Creare nuovi oggetti copiando un prototipo, specificando i tipi da creare con un'istanza.
Quando. Le classi da istanziare sono note solo a runtime (configurazione); si vuole evitare una gerarchia di fabbriche parallela a quella dei prodotti; le istanze di una classe hanno poche combinazioni di stato e conviene copiarne una già configurata.
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Prototype | Documento (con clone()) |
dichiara l'operazione di copia |
| ConcretePrototype | Libro, Dvd |
implementano la copia |
| Client / registro | RegistroPrototipi |
crea nuovi oggetti chiedendo la copia di un prototipo registrato per nome |
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Prototype: ogni prodotto sa produrre una copia di se' stesso. */
abstract class Documento implements Cloneable {
protected String titolo;
protected List<String> paroleChiave = new ArrayList<>(); // oggetto mutabile: serve la copia profonda
protected int copie = 1;
@Override public Documento clone() {
try {
Documento c = (Documento) super.clone(); // copia superficiale: condivide la lista
c.paroleChiave = new ArrayList<>(paroleChiave); // copia profonda della parte mutabile
return c;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // non succede: implementiamo Cloneable
}
}
@Override public String toString() {
return getClass().getSimpleName() + "(" + titolo + ", " + paroleChiave + ", copie=" + copie + ")";
}
}
class Libro extends Documento {
Libro(String titolo, String... parole) { this.titolo = titolo; for (String p : parole) paroleChiave.add(p); }
}
class Dvd extends Documento {
Dvd(String titolo, String... parole) { this.titolo = titolo; for (String p : parole) paroleChiave.add(p); }
}
/** Registro di prototipi: crea nuovi documenti clonando quello registrato con il nome richiesto. */
class RegistroPrototipi {
private final Map<String, Documento> prototipi = new HashMap<>();
void registra(String nome, Documento p) { prototipi.put(nome, p); }
Documento crea(String nome) {
Documento p = prototipi.get(nome);
if (p == null) throw new IllegalArgumentException("prototipo sconosciuto: " + nome);
return p.clone();
}
}
public class PrototypeEx {
public static void main(String[] args) {
RegistroPrototipi reg = new RegistroPrototipi();
reg.registra("manuale", new Libro("Manuale di Java", "java", "programmazione"));
reg.registra("film", new Dvd("Titolo da definire", "cinema"));
Documento a = reg.crea("manuale"); // niente "new Libro(...)": la classe concreta e' nascosta
Documento b = reg.crea("manuale");
b.titolo = "Manuale di Java, 2a edizione";
b.paroleChiave.add("seconda-edizione");
b.copie = 3;
System.out.println(a);
System.out.println(b);
System.out.println("a e b sono lo stesso oggetto? " + (a == b));
System.out.println("stessa classe? " + (a.getClass() == b.getClass()));
System.out.println(reg.crea("film"));
}
}Libro(Manuale di Java, [java, programmazione], copie=1)
Libro(Manuale di Java, 2a edizione, [java, programmazione, seconda-edizione], copie=3)
a e b sono lo stesso oggetto? false
stessa classe? true
Dvd(Titolo da definire, [cinema], copie=1)In Java si implementa Cloneable e si ridefinisce clone(): super.clone() fa una copia superficiale (i campi mutabili, come la lista paroleChiave, restano condivisi tra originale e copia), quindi le parti mutabili vanno copiate a mano (copia profonda). In molti casi un costruttore di copia (new Documento(altro)) è più chiaro del meccanismo Cloneable.
Scelta tra i creazionali
| esigenza | pattern |
|---|---|
| un'unica istanza | Singleton |
| la sottoclasse decide che cosa creare | Factory Method |
| più oggetti da creare insieme e coerenti | Abstract Factory |
| molti parametri e campi facoltativi | Builder |
| copiare un oggetto già configurato | Prototype |
| una sola scelta condizionale semplice | Simple Factory (non è GoF) |
Applicazione completa con scelta motivata: Esercizio - Abstract Factory per le ricevute di prestito (progetto originale).
Domande d'esame
1. Perché il Singleton lazy con un semplice if (istanza == null) non è corretto con più thread? Elencare tre rimedi.
Traccia. Due thread possono vedere null insieme e creare due istanze. Rimedi: metodo synchronized (corretto, ma costoso a ogni chiamata), inizializzazione eager (campo static final), double-checked locking con campo volatile, holder idiom, enum. Con la riflessione (setAccessible) si può comunque creare una seconda istanza, tranne che con un enum.
2. Differenza tra Factory Method e Abstract Factory. Quando si usa l'uno e quando l'altro? Traccia. Factory Method: un solo prodotto, scelto dalla sottoclasse di un Creator (ereditarietà). Abstract Factory: una famiglia di prodotti correlati prodotta da un oggetto-fabbrica (composizione), per garantire coerenza; aggiungere un tipo di prodotto richiede di cambiare l'interfaccia. Si usa il primo quando la variabilità è su un tipo di oggetto, il secondo quando più oggetti devono variare insieme.
3. Un oggetto ha 3 campi obbligatori e 6 facoltativi, e deve essere immutabile. Quale pattern si usa e come si implementa?
Traccia. Builder: classe interna statica con i campi obbligatori nel costruttore e un metodo per ogni facoltativo che restituisce this; build() controlla i vincoli e crea l'oggetto con un costruttore privato che copia i campi del builder (campi final); così non servono costruttori con molti parametri né setter.
Versione ripasso
- Creazionali: isolano la creazione; il client dipende solo da interfacce. Problema di
new: lega alla classe concreta, unifdi creazione dipende da tutte le classi.
| pattern | intento | ruoli |
|---|---|---|
| Singleton | una sola istanza, accesso globale | costruttore privato, campo statico, getInstance() |
| Factory Method | la sottoclasse sceglie la classe da creare | Product, ConcreteProduct, Creator (metodo astratto), ConcreteCreator |
| Abstract Factory | famiglie di prodotti correlati | AbstractFactory, ConcreteFactory, AbstractProduct, ConcreteProduct, Client |
| Builder | costruire passo passo, campi facoltativi, immutabile | Product (costruttore privato), Builder (fluente, build()), Director |
| Prototype | creare copiando un prototipo | Prototype (clone), ConcretePrototype, registro |
- Singleton lazy ingenuo: non thread-safe (due istanze). Varianti:
synchronized(corretto, lento), eager (static final), double-checked locking convolatile(senzavolatileè sbagliato), holder idiom, enum (immune a riflessione e serializzazione). La riflessione consetAccessible(true)rompe il costruttore privato (non l'enum).
private static volatile ConfigDCL istanza;
static ConfigDCL get() {
ConfigDCL r = istanza;
if (r == null) {
synchronized (ConfigDCL.class) {
r = istanza;
if (r == null) istanza = r = new ConfigDCL();
}
}
return r; // fuori dall'if
}| variante di Singleton | come | pro e contro |
|---|---|---|
| lazy ingenuo | if (istanza == null) istanza = new ... |
non thread-safe |
synchronized |
static synchronized getInstance() |
corretto, ogni chiamata paga il lock |
| eager | private static final X ISTANZA = new X(); |
thread-safe, creata anche se non serve |
| double-checked locking | doppio if, synchronized solo la prima volta, volatile |
lazy e veloce, volatile indispensabile |
| holder idiom | classe interna statica con l'istanza | lazy, senza lock |
| enum | enum Config { ISTANZA; } |
thread-safe, immune a riflessione e serializzazione, non ereditabile |
- Linee guida del corso: prestazioni irrilevanti o un solo thread versione semplice; creazione economica eager; costosa double-checked locking.
- Quando usare il Singleton: risorsa unica per natura (configurazione, spooler, registro, pool di connessioni). Dichiara l'unicità e permette lazy initialization rispetto a una variabile
static; non usarlo come variabile globale comoda (dipendenze nascoste, test difficili). - Applicabilità dei pattern:
- Factory Method: la classe non può prevedere la classe degli oggetti che crea; vuole che la scelgano le sottoclassi; localizza la scelta in un punto.
- Abstract Factory: sistema indipendente dalla creazione dei prodotti; configurato con una famiglia tra più; prodotti progettati per andare insieme.
- Builder: molti parametri facoltativi; oggetto immutabile costruito in più passi; validazione dell'insieme dei campi.
- Prototype: classi note solo a runtime; evitare gerarchie di fabbriche parallele; poche combinazioni di stato.
- Errori del corso: slide Singleton lazy:
return new ...senza assegnare (ogni chiamata crea un oggetto:get() == get()èfalse); DCL della dispensa:returndentro l'if(manca ilreturn,missing return statement);toStringsenza>;createPizzasenzareturnfinale e dichiarazione senza tipo di ritorno. - Simple Factory: l'
ifin una classe a parte; non è GoF; da modificare a ogni nuovo tipo. - Factory Method: il Creator ha il codice comune (
acquisisci) e il metodo astrattocreaDocumento; gerarchia parallela Creator/Product; il codice comune dipende solo daDocumento(DIP). Variante: parametrizzato, con default;List.ofeInteger.valueOfsono static factory.
abstract class Biblioteca {
protected abstract Documento creaDocumento(String tipo, String titolo); // factory method
public Documento acquisisci(String tipo, String titolo) {
Documento d = creaDocumento(tipo, titolo); // codice comune
catalogo.add(d);
return d;
}
}- Abstract Factory: una fabbrica per famiglia (cartacea:
LibroCartaceo+SchedaScaffale; digitale:EBook+LicenzaDigitale); il client (Catalogatore) non nomina classi concrete e non può mescolare le famiglie; cambiare famiglia = un punto solo; aggiungere un prodotto è difficile (cambia l'interfaccia). Factory Method = ereditarietà, Abstract Factory = composizione; si usano insieme (pizzeria: stile regionale e ingredienti). JDBC è un abstract factory.
interface FabbricaSupporto { // AbstractFactory: un metodo per ogni prodotto
Documento creaDocumento(String titolo);
Accesso creaAccesso(String riferimento);
}
class FabbricaDigitale implements FabbricaSupporto {
public Documento creaDocumento(String t) { return new EBook(t); }
public Accesso creaAccesso(String r) { return new LicenzaDigitale(r); }
}
class Catalogatore { // il client conosce solo le interfacce
private final FabbricaSupporto fabbrica;
Catalogatore(FabbricaSupporto f) { this.fabbrica = f; }
String scheda(String titolo, String rif) {
return fabbrica.creaDocumento(titolo).descrizione() + " -> " + fabbrica.creaAccesso(rif).istruzioni();
}
} Uscite: libro cartaceo 'Se questo e' un uomo' -> ritira al banco, scaffale B-12 e e-book 'Se questo e' un uomo' (EPUB) -> scarica con la licenza LIC-8841 (scade in 14 giorni).
- Builder: evita i costruttori telescopici (
new Ricerca("rosa", null, 1980, null, true, null));new RichiestaRicerca.Builder("rosa").autore("Eco").dal(1980).al(1990).soloDisponibili().build();build()controlla le regole tra campi (anni invertiti, anno futuro:IllegalStateException); il prodotto resta immutabile.
final class RichiestaRicerca {
private RichiestaRicerca(Builder b) { /* copia i campi */ }
static class Builder {
Builder(String parolaChiave) { /* obbligatoria: controlla */ }
Builder dal(int anno) { this.annoDa = anno; return this; }
RichiestaRicerca build() {
if (annoDa != null && annoA != null && annoDa > annoA)
throw new IllegalStateException("intervallo di anni invertito");
return new RichiestaRicerca(this);
}
}
}- Prototype:
Cloneable+clone();super.clone()è superficiale: le parti mutabili (liste) vanno copiate a mano (copia profonda); alternativa: costruttore di copia; registro di prototipi per nome.
@Override public Documento clone() {
try {
Documento c = (Documento) super.clone(); // copia superficiale
c.paroleChiave = new ArrayList<>(paroleChiave); // copia profonda della parte mutabile
return c;
} catch (CloneNotSupportedException e) { throw new AssertionError(e); }
}- Uscite dei programmi: Factory Method:
BibliotecaCivica acquisisce Libro(Il barone rampante), prestito 30 giorni,Rivista(Le Scienze)7 giorni,Mediateca ... Libro(...)45 giorni,Dvd(Metropolis)3 giorni,errore: la civica non acquisisce: dvd. Prototype: la copiabcon titolo e parole chiave modificati non cambiaa;a == bèfalse, stessa classe. Singleton: ingenuo fino a 8 istanze (varia), double-checked 1; holder dopo riflessione:ancora unica? false; enum:Cannot reflectively create enum objects. - Quale usare: un'istanza Singleton; sottoclasse decide Factory Method; oggetti coerenti insieme Abstract Factory; molti parametri facoltativi Builder; copiare Prototype. Esercizio: Esercizio - Abstract Factory per le ricevute di prestito (progetto originale).
- Errori tipici: Singleton lazy senza sincronizzazione; DCL senza
volatile; Singleton come variabile globale (test impossibili); Abstract Factory per un solo prodotto; copia superficiale di liste nel Prototype; Builder senza controlli inbuild().