Salta al contenuto
Note per Studenti Esercizio - Abstract Factory per le ricevute di prestito (progetto originale)

Esercizio - Abstract Factory per le ricevute di prestito (progetto originale)

Esercizio originale, nello stile del progetto d'esame (scelta e applicazione di un pattern creazionale, con il codice). Teoria: Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →. Il codice è stato compilato ed eseguito con JDK 21.

Testo

Alla registrazione di un prestito il sistema della biblioteca stampa una ricevuta, composta da tre parti: l'intestazione (nome della biblioteca e data), una riga per ogni documento (titolo e scadenza) e il piede (totale dei documenti). Oggi serve il formato testo semplice (stampante dello sportello); tra poco servirà l'HTML (invio per posta elettronica) e in seguito il PDF. Le tre parti devono essere dello stesso formato: una ricevuta con intestazione HTML e righe di testo semplice sarebbe rotta.

Si richiede di: 1) scegliere, tra Simple Factory, Factory Method, Abstract Factory, Builder e Singleton, il pattern adatto, motivando perché si scartano gli altri; 2) mostrare i ruoli; 3) scrivere il codice Java completo con un main che produca la stessa ricevuta nei due formati; 4) dire che cosa bisogna modificare per aggiungere il PDF e che cosa invece sarebbe difficile da aggiungere.

Soluzione

1. Scelta

Il requisito decisivo è "le tre parti devono essere dello stesso formato": serve creare una famiglia di oggetti correlati (intestazione, riga, piede) garantendo che ne venga usata una sola famiglia alla volta, senza che il codice che compone la ricevuta nomini classi concrete. È l'Abstract Factory.

pattern perché si scarta o in che ruolo entra
Simple Factory un if sul formato in una classe: va modificata a ogni nuovo formato e non garantisce la coerenza (si potrebbe chiedere intestazione HTML e piede di testo)
Factory Method produce un prodotto scelto da una sottoclasse; qui i prodotti sono tre e devono essere coerenti
Builder serve a costruire un oggetto complesso passo per passo, non a scegliere la famiglia di classi; si potrebbe usare dentro il generatore per costruire il testo, ma qui basta uno StringBuilder
Singleton non c'entra con la scelta del formato; le fabbriche concrete sono stateless e si potrebbero rendere Singleton, ma non è richiesto

2. Ruoli

ruolo classe
AbstractFactory FabbricaRicevuta (tre metodi di creazione)
ConcreteFactory FabbricaTesto, FabbricaHtml
AbstractProduct Intestazione, RigaDocumento, Piede
ConcreteProduct le espressioni lambda delle due fabbriche (le interfacce hanno un solo metodo)
Client GeneratoreRicevute (compone la ricevuta con la fabbrica ricevuta nel costruttore)

3. Codice

java
import java.time.LocalDate;
import java.util.List;

/** Prodotti della famiglia "ricevuta": devono usare tutti lo stesso formato. */
interface Intestazione { String rendi(String biblioteca, LocalDate data); }
interface RigaDocumento { String rendi(String titolo, LocalDate scadenza); }
interface Piede { String rendi(int totaleDocumenti); }

/** Abstract Factory. */
interface FabbricaRicevuta {
    Intestazione creaIntestazione();
    RigaDocumento creaRiga();
    Piede creaPiede();
}

/* ---- famiglia TESTO ---- */
class FabbricaTesto implements FabbricaRicevuta {
    public Intestazione creaIntestazione() { return (b, d) -> "== " + b + " == " + d; }
    public RigaDocumento creaRiga()        { return (t, s) -> "* " + t + " (entro il " + s + ")"; }
    public Piede creaPiede()               { return n -> "totale: " + n; }
}

/* ---- famiglia HTML ---- */
class FabbricaHtml implements FabbricaRicevuta {
    public Intestazione creaIntestazione() { return (b, d) -> "<h1>" + b + "</h1><p>" + d + "</p><ul>"; }
    public RigaDocumento creaRiga()        { return (t, s) -> "<li>" + t + " &mdash; entro il " + s + "</li>"; }
    public Piede creaPiede()               { return n -> "</ul><p>totale: " + n + "</p>"; }
}

/** Client: compone la ricevuta senza sapere in quale formato. Per aggiungere il PDF basta una nuova fabbrica. */
class GeneratoreRicevute {
    private final FabbricaRicevuta fabbrica;
    GeneratoreRicevute(FabbricaRicevuta fabbrica) { this.fabbrica = fabbrica; }

    String genera(String biblioteca, LocalDate oggi, List<String> titoli, int giorni) {
        StringBuilder sb = new StringBuilder();
        sb.append(fabbrica.creaIntestazione().rendi(biblioteca, oggi)).append('\n');
        RigaDocumento riga = fabbrica.creaRiga();
        for (String t : titoli) sb.append(riga.rendi(t, oggi.plusDays(giorni))).append('\n');
        sb.append(fabbrica.creaPiede().rendi(titoli.size()));
        return sb.toString();
    }
}

public class Ricevute {
    public static void main(String[] args) {
        LocalDate oggi = LocalDate.of(2026, 10, 14);
        List<String> titoli = List.of("Il nome della rosa", "Baudolino");
        System.out.println(new GeneratoreRicevute(new FabbricaTesto()).genera("Civica", oggi, titoli, 30));
        System.out.println();
        System.out.println(new GeneratoreRicevute(new FabbricaHtml()).genera("Civica", oggi, titoli, 30));
    }
}

Uscita:

== Civica == 2026-10-14
* Il nome della rosa (entro il 2026-11-13)
* Baudolino (entro il 2026-11-13)
totale: 2

<h1>Civica</h1><p>2026-10-14</p><ul>
<li>Il nome della rosa &mdash; entro il 2026-11-13</li>
<li>Baudolino &mdash; entro il 2026-11-13</li>
</ul><p>totale: 2</p>

Il client GeneratoreRicevute non nomina nessuna classe concreta: riceve la fabbrica da fuori (qui in main, in un programma vero da una configurazione) ed è quindi impossibile mescolare i formati, perché tutte e tre le parti vengono dalla stessa fabbrica. I prodotti sono interfacce con un solo metodo, quindi basta una lambda: il pattern non richiede una classe per ogni prodotto.

4. Estensione e limiti

  • Aggiungere il PDF: una classe FabbricaPdf che implementa FabbricaRicevuta; nessuna modifica a GeneratoreRicevute né alle altre fabbriche, e il punto in cui si sceglie la fabbrica (main) cambia di una riga: è il principio aperto/chiuso.
  • Aggiungere un nuovo tipo di prodotto (per esempio un Logo o una NotaLegale) è difficile: bisogna aggiungere un metodo all'interfaccia FabbricaRicevuta e quindi a tutte le fabbriche concrete esistenti, e modificare il client. È il limite noto dell'Abstract Factory: rende facile aggiungere famiglie, difficile aggiungere prodotti.
  • Rapporto con il Factory Method: ognuno dei tre metodi crea... è un factory method; l'Abstract Factory è basata su composizione (la fabbrica è un oggetto passato al client), il Factory Method su ereditarietà.

Versione ripasso

  • Requisito decisivo: tre parti (intestazione, riga, piede) dello stesso formato →\to famiglia di oggetti correlati →\to Abstract Factory.
  • Scarti: Simple Factory (un if, nessuna coerenza, da modificare a ogni formato); Factory Method (un solo prodotto, per sottoclasse); Builder (costruisce un oggetto passo per passo, non sceglie la famiglia); Singleton (non c'entra con il formato).
  • Ruoli: AbstractFactory FabbricaRicevuta; ConcreteFactory FabbricaTesto, FabbricaHtml; AbstractProduct Intestazione, RigaDocumento, Piede; Client GeneratoreRicevute.
java
interface FabbricaRicevuta {
    Intestazione creaIntestazione();
    RigaDocumento creaRiga();
    Piede creaPiede();
}
class FabbricaHtml implements FabbricaRicevuta {
    public Intestazione creaIntestazione() { return (b, d) -> "<h1>" + b + "</h1><p>" + d + "</p><ul>"; }
    public RigaDocumento creaRiga()        { return (t, s) -> "<li>" + t + " &mdash; entro il " + s + "</li>"; }
    public Piede creaPiede()               { return n -> "</ul><p>totale: " + n + "</p>"; }
}
class GeneratoreRicevute {
    private final FabbricaRicevuta fabbrica;               // ricevuta da fuori: nessuna classe concreta nominata
    GeneratoreRicevute(FabbricaRicevuta f) { this.fabbrica = f; }
}
  • Uscita (testo): == Civica == 2026-10-14, * Il nome della rosa (entro il 2026-11-13), * Baudolino (entro il 2026-11-13), totale: 2; la stessa ricevuta in HTML con <h1>, <li> e </ul><p>totale: 2</p>.
  • Estensione: aggiungere il PDF = nuova FabbricaPdf, nessuna modifica al client (aperto/chiuso). Aggiungere un prodotto (logo, nota legale) = cambia l'interfaccia e tutte le fabbriche: è il limite dell'Abstract Factory.
  • Factory Method e Abstract Factory: i metodi crea... sono factory method; Abstract Factory = composizione, Factory Method = ereditarietà.
  • Errori tipici: usare una Simple Factory e credere di avere la coerenza della famiglia; far nominare al client una classe concreta; aggiungere un prodotto senza aggiornare tutte le fabbriche; usare l'Abstract Factory quando il prodotto è uno solo.

Teoria collegata