Salta al contenuto
Note per Studenti Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype

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 è

java
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 versione synchronized della stessa slide ha l'errore. La dispensa corregge l'assegnamento. (2) Il double-checked locking della dispensa ha return uniqueInstance; dentro l'if: quando l'istanza esiste, il metodo non restituisce niente e il compilatore dà missing return statement. Il return va fuori dall'if, come nel programma sotto. (3) Nel toString del Singleton del corso manca il > di chiusura.

java
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 objects

La 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 (CodaG) spostano i controlli di tipo a tempo di compilazione ma sono cancellati dalla type erasure, quindi non sono i template del C++; la riflessione (Class, Constructor, Method, Field) permette di caricare classi per nome, creare oggetti, chiamare metodi e leggere o scrivere campi a tempo di esecuzione, con eccezioni specifiche (ClassNotFoundException, InvocationTargetException, IllegalAccessException) e il costo di aggirare il controllo statico.Java - generics, reflection e passaggio dei parametri →); l'enum 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 di if/else if e nessun return finale: non compila (missing return statement); nella classe astratta PizzaStore la dichiarazione protected 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.

java
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: dvd

Varianti. 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
java
/** 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
java
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 > 1990

Il 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
java
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, un if di 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 con volatile (senza volatile è sbagliato), holder idiom, enum (immune a riflessione e serializzazione). La riflessione con setAccessible(true) rompe il costruttore privato (non l'enum).
java
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 →\to versione semplice; creazione economica →\to eager; costosa →\to 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: return dentro l'if (manca il return, missing return statement); toString senza >; createPizza senza return finale e dichiarazione senza tipo di ritorno.
  • Simple Factory: l'if in una classe a parte; non è GoF; da modificare a ogni nuovo tipo.
  • Factory Method: il Creator ha il codice comune (acquisisci) e il metodo astratto creaDocumento; gerarchia parallela Creator/Product; il codice comune dipende solo da Documento (DIP). Variante: parametrizzato, con default; List.of e Integer.valueOf sono static factory.
java
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.
java
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.
java
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.
java
@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 copia b con titolo e parole chiave modificati non cambia a; 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 →\to Singleton; sottoclasse decide →\to Factory Method; oggetti coerenti insieme →\to Abstract Factory; molti parametri facoltativi →\to Builder; copiare →\to 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 in build().

Esercizi su questo argomento

Teoria collegata