Pattern comportamentali - Observer, Strategy, Command, Iterator e Template Method
In questa pagina 7
Definizione. I pattern comportamentali riguardano le interazioni dinamiche tra gruppi di classi e oggetti e come distribuiscono le responsabilità (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 undici; qui sono trattati con esempio completo Observer, Strategy, Command, Iterator e Template Method, gli altri sei sono descritti in fondo. Esempi sul dominio biblioteca, compilati ed eseguiti con JDK 21.
| pattern | in una riga | che cosa si rende variabile o disaccoppiato |
|---|---|---|
| Observer | quando un oggetto cambia, i dipendenti sono avvisati | chi ascolta (numero e tipo di osservatori) |
| Strategy | un algoritmo scelto tra una famiglia | l'algoritmo |
| Command | la richiesta è un oggetto | quando e da chi è eseguita (coda, undo) |
| Iterator | scorrere senza conoscere la struttura | il modo di scorrere |
| Template Method | scheletro fisso, passi variabili | i passi dell'algoritmo |
Observer
Intento. Definire una dipendenza uno-a-molti tra oggetti: quando un oggetto (il soggetto) cambia stato o genera un evento, tutti gli osservatori sono avvisati e si aggiornano automaticamente. Also known as: Publish-Subscribe (publisher e subscriber).
Problema. Diversi tipi di subscriber sono interessati agli eventi di un publisher e vogliono reagire ognuno a modo proprio; il publisher vuole mantenere un basso accoppiamento con loro. Soluzione: un'interfaccia per i listener, che i subscriber implementano; il publisher li registra e li cancella dinamicamente e li notifica quando si verifica un evento. Vale lo stile architetturale a eventi di Architettura del software - decisioni, organizzazione e stiliL'architettura del software descrive come un sistema è organizzato in componenti, le loro relazioni e la loro distribuzione; è il ponte tra requisiti e progetto dettagliato, e le sue decisioni derivano soprattutto dai requisiti non funzionali (prestazioni, sicurezza, disponibilità, manutenibilità), spesso in conflitto tra loro; si descrive con più viste (logica, processi, sviluppo, fisica); gli stili di organizzazione ricorrenti sono a livelli (layered), MVC, repository, client-server, pipe and filter, publish-subscribe e microservizi; la modularizzazione mira ad alta coesione e basso accoppiamento, nascondendo le decisioni dietro interfacce (information hiding, inversione delle dipendenze).Architettura del software - decisioni, organizzazione e stili →.
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Subject (osservato) | Soggetto<E> |
tiene la lista degli osservatori; registra, cancella, notifica(evento) |
| ConcreteSubject | Documento |
cambia stato (restituisci) e chiama notifica; non sa chi ascolta |
| Observer | Osservatore<E> |
interfaccia con aggiorna(evento) |
| ConcreteObserver | PrenotazioneUtente, StatisticheRestituzioni, una lambda |
reagiscono in modo diverso allo stesso evento |
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/** Observer: chi vuole essere avvisato implementa questa interfaccia. */
interface Osservatore<E> {
void aggiorna(E evento);
}
/** Subject: tiene la lista degli osservatori, che si registrano e cancellano a runtime. */
class Soggetto<E> {
// CopyOnWriteArrayList: si puo' (de)registrare mentre si sta notificando, anche da altri thread
private final List<Osservatore<E>> osservatori = new CopyOnWriteArrayList<>();
boolean registra(Osservatore<E> o) { return osservatori.add(o); }
boolean cancella(Osservatore<E> o) { return osservatori.remove(o); }
int numeroOsservatori() { return osservatori.size(); }
protected void notifica(E evento) {
for (Osservatore<E> o : osservatori) o.aggiorna(evento);
}
}
/** Evento passato agli osservatori (modello "push"). */
class CopiaRestituita {
final String titolo;
CopiaRestituita(String titolo) { this.titolo = titolo; }
}
/** ConcreteSubject. */
class Documento extends Soggetto<CopiaRestituita> {
private final String titolo;
private boolean inPrestito = true;
Documento(String titolo) { this.titolo = titolo; }
void restituisci() {
inPrestito = false;
notifica(new CopiaRestituita(titolo)); // il documento non sa chi ascolta
}
boolean inPrestito() { return inPrestito; }
}
/** ConcreteObserver 1. */
class PrenotazioneUtente implements Osservatore<CopiaRestituita> {
private final String utente;
PrenotazioneUtente(String utente) { this.utente = utente; }
public void aggiorna(CopiaRestituita e) {
System.out.println("mail a " + utente + ": '" + e.titolo + "' e' tornato disponibile");
}
}
/** ConcreteObserver 2: un tipo di osservatore totalmente diverso, senza modificare Documento. */
class StatisticheRestituzioni implements Osservatore<CopiaRestituita> {
int restituzioni = 0;
public void aggiorna(CopiaRestituita e) { restituzioni++; }
}
public class ObserverEx {
public static void main(String[] args) {
Documento rosa = new Documento("Il nome della rosa");
PrenotazioneUtente anna = new PrenotazioneUtente("anna");
PrenotazioneUtente luca = new PrenotazioneUtente("luca");
StatisticheRestituzioni stat = new StatisticheRestituzioni();
rosa.registra(anna);
rosa.registra(luca);
rosa.registra(stat);
rosa.cancella(luca); // luca ha cambiato idea
rosa.registra(e -> System.out.println("log: restituito " + e.titolo)); // lambda: Osservatore e' funzionale
rosa.restituisci();
System.out.println("restituzioni contate: " + stat.restituzioni + ", osservatori: " + rosa.numeroOsservatori());
}
}mail a anna: 'Il nome della rosa' e' tornato disponibile
log: restituito Il nome della rosa
restituzioni contate: 1, osservatori: 3Punti di progetto.
- Push o pull. Nel push il soggetto passa i dati nell'evento (
CopiaRestituita); nel pull passa se stesso e gli osservatori leggono ciò che serve (l'esercizio Esercizio - Observer per le prenotazioni dei documenti (progetto originale) usa il pull). - Modifica durante la notifica. Il codice del corso (
OsservatoImpl: lista concatenata eListIterator) lanciaConcurrentModificationExceptionse un osservatore si cancella dentrodeliver(provato). ConCopyOnWriteArrayListsi itera su una copia: sicuro anche con più thread (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 →). Il corso non impedisce neppure la registrazione doppia (registeraggiunge sempre). - Ordine e affidabilità. Gli osservatori sono avvisati nell'ordine di registrazione; se uno lancia un'eccezione gli altri non vengono avvisati, a meno di catturarla; una notifica lunga blocca il soggetto (si può notificare in un altro thread, con un pool).
- Dipendenze ciclicamente nascoste. Un osservatore che modifica il soggetto può far scattare notifiche a catena.
- Nel JDK.
java.util.Observable/Observersono deprecati (da Java 9); si usanojava.beans.PropertyChangeListener, i listener della GUI, ojava.util.concurrent.Flowper flussi reattivi. Lo stesso schema è alla base dell'MVC (JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller →), dove la vista si aggiorna ai cambi del modello.
Nota sul codice del corso. Il file
Calculatordella dispensa, usato come primo passo del ciclo di sviluppo guidato dai test, termina conreturn 0;invece direturn sum;: è intenzionale per vedere fallire il test (red) prima di correggerlo (green), come spiegato in Software testing - progettazione dei casi di test e JUnitIl testing esegue il programma con dati artificiali per mostrare che soddisfa i requisiti (validation testing) e per scoprirne i difetti (defect testing), ma può rivelare la presenza di errori e non la loro assenza; il test di sviluppo comprende unit test (singole classi e metodi), test dei componenti (le interfacce) e test di sistema (le interazioni); i casi di test si ricavano con le classi di equivalenza (una per partizione) e i valori limite (ai bordi delle partizioni), più linee guida per sequenze e input anomali; JUnit automatizza i test con @Test, fixture @Before/@After (@BeforeClass/@AfterClass), le asserzioni (assertEquals con atteso per primo), eccezioni attese, timeout e @Ignore; nel TDD il test si scrive prima del codice (red, green, refactor) e i test automatici diventano la suite di regressione; seguono i test di rilascio (black box sui requisiti, prestazioni) e dell'utente (alpha, beta, accettazione).Software testing - progettazione dei casi di test e JUnit →.
Strategy
Intento. Definire una famiglia di algoritmi, incapsularli uno per uno e renderli intercambiabili: l'algoritmo varia indipendentemente dai client che lo usano.
Quando. Molte classi correlate differiscono solo per il comportamento; servono varianti di un algoritmo (calcolo di una tariffa, ordinamento); si vogliono togliere lunghi if/switch sul tipo; si vuole cambiare l'algoritmo a runtime.
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Strategy | PoliticaMulta |
interfaccia dell'algoritmo (calcola(giorniRitardo)) |
| ConcreteStrategy | MultaStudente, MultaDocente, MultaEsterno |
una variante ciascuna |
| Context | Utente |
tiene una Strategy, la usa senza sapere quale sia, permette di cambiarla |
import java.util.Locale;
/** Strategy: una famiglia di algoritmi intercambiabili dietro la stessa interfaccia. */
@FunctionalInterface
interface PoliticaMulta {
double calcola(int giorniRitardo);
}
class MultaStudente implements PoliticaMulta {
public double calcola(int g) { return g <= 0 ? 0 : 0.10 * g; }
}
class MultaDocente implements PoliticaMulta {
public double calcola(int g) { return g <= 7 ? 0 : 0.20 * (g - 7); } // una settimana di tolleranza
}
class MultaEsterno implements PoliticaMulta {
public double calcola(int g) { return g <= 0 ? 0 : Math.min(0.50 * g, 15.0); } // tetto massimo
}
/** Context: usa la strategia senza sapere quale sia, e la si puo' cambiare a runtime. */
class Utente {
private final String nome;
private PoliticaMulta politica;
Utente(String nome, PoliticaMulta politica) { this.nome = nome; this.politica = politica; }
void cambiaPolitica(PoliticaMulta p) { this.politica = p; }
String conto(int giorniRitardo) {
return String.format(Locale.ROOT, "%s: %d giorni di ritardo -> %.2f euro", nome, giorniRitardo, politica.calcola(giorniRitardo));
}
}
public class StrategyEx {
public static void main(String[] args) {
Utente anna = new Utente("anna (studente)", new MultaStudente());
Utente prof = new Utente("prof (docente)", new MultaDocente());
Utente ospite = new Utente("ospite", new MultaEsterno());
System.out.println(anna.conto(10));
System.out.println(prof.conto(10));
System.out.println(ospite.conto(40)); // 20.00 ridotto al tetto di 15.00
// la strategia e' un'interfaccia con un solo metodo: basta una lambda
ospite.cambiaPolitica(g -> 0); // amnistia
System.out.println(ospite.conto(40));
}
}anna (studente): 10 giorni di ritardo -> 1.00 euro
prof (docente): 10 giorni di ritardo -> 0.60 euro
ospite: 40 giorni di ritardo -> 15.00 euro
ospite: 40 giorni di ritardo -> 0.00 euroCon un'interfaccia a un solo metodo (@FunctionalInterface) la strategia si scrive con una lambda (g -> 0). Strategy e Template Method risolvono lo stesso problema con strumenti diversi: Strategy usa la composizione (l'algoritmo è un oggetto che si sostituisce, anche a runtime), Template Method l'ereditarietà (i passi sono metodi delle sottoclassi, fissi alla creazione). Strategy e State hanno la stessa struttura, ma in State è l'oggetto stesso a cambiare strategia al variare dello stato.
Command
Intento. Incapsulare una richiesta in un oggetto, così da parametrizzare i client con richieste diverse, accodarle, registrarle e supportare l'annullamento (undo).
Quando. Si vogliono azioni annullabili, code di operazioni, registro (log) delle azioni da ripetere, operazioni da eseguire in un altro thread o più tardi; si vuole disaccoppiare chi invoca da chi esegue.
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Command | Comando |
interfaccia esegui() e annulla() |
| ConcreteCommand | Prestito, Restituzione |
legano un Receiver a un'azione e sanno annullarla |
| Receiver | Magazzino |
sa fare il lavoro vero (prendi, rimetti) |
| Invoker | Sportello |
esegue i comandi, tiene lo storico per l'undo |
| Client | main |
crea i comandi con il Receiver |
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
/** Receiver: sa fare il lavoro vero. */
class Magazzino {
private final Map<String, Integer> copie = new HashMap<>(Map.of("rosa", 2));
void prendi(String t) {
if (copie.getOrDefault(t, 0) == 0) throw new IllegalStateException("nessuna copia di " + t);
copie.merge(t, -1, Integer::sum);
}
void rimetti(String t) { copie.merge(t, 1, Integer::sum); }
int copie(String t) { return copie.getOrDefault(t, 0); }
}
/** Command: la richiesta diventa un oggetto, quindi si puo' memorizzare, mettere in coda e annullare. */
interface Comando {
void esegui();
void annulla();
}
class Prestito implements Comando {
private final Magazzino m; private final String titolo;
Prestito(Magazzino m, String titolo) { this.m = m; this.titolo = titolo; }
public void esegui() { m.prendi(titolo); }
public void annulla() { m.rimetti(titolo); }
@Override public String toString() { return "prestito(" + titolo + ")"; }
}
class Restituzione implements Comando {
private final Magazzino m; private final String titolo;
Restituzione(Magazzino m, String titolo) { this.m = m; this.titolo = titolo; }
public void esegui() { m.rimetti(titolo); }
public void annulla() { m.prendi(titolo); }
@Override public String toString() { return "restituzione(" + titolo + ")"; }
}
/** Invoker: esegue i comandi senza conoscerne il tipo e tiene lo storico per l'undo. */
class Sportello {
private final Deque<Comando> storico = new ArrayDeque<>();
void esegui(Comando c) {
c.esegui(); // se lancia, il comando non entra nello storico
storico.push(c);
System.out.println("eseguito " + c);
}
void annullaUltimo() {
if (storico.isEmpty()) { System.out.println("niente da annullare"); return; }
Comando c = storico.pop();
c.annulla();
System.out.println("annullato " + c);
}
}
public class CommandEx {
public static void main(String[] args) {
Magazzino m = new Magazzino();
Sportello s = new Sportello();
s.esegui(new Prestito(m, "rosa"));
s.esegui(new Prestito(m, "rosa"));
System.out.println("copie: " + m.copie("rosa"));
try {
s.esegui(new Prestito(m, "rosa")); // finite
} catch (IllegalStateException e) {
System.out.println("errore: " + e.getMessage());
}
s.annullaUltimo();
s.annullaUltimo();
System.out.println("copie dopo due undo: " + m.copie("rosa"));
s.annullaUltimo();
}
}eseguito prestito(rosa)
eseguito prestito(rosa)
copie: 0
errore: nessuna copia di rosa
annullato prestito(rosa)
annullato prestito(rosa)
copie dopo due undo: 2
niente da annullareUn comando che lancia non entra nello storico (l'errore avviene prima del push), e l'annullamento di un comando riuscito ripristina lo stato (le copie tornano a 2). Variante: macro-comandi (un comando composto da altri, pattern Composite) e comandi che salvano lo stato precedente (con un Memento) invece di calcolare l'inverso.
Iterator
Intento. Fornire un modo per scorrere gli elementi di un aggregato in sequenza senza esporne la rappresentazione interna.
Quando. Si vogliono più modi di scorrere lo stesso aggregato (tutti, solo i disponibili), contemporaneamente; un'interfaccia uniforme per aggregati diversi (array, liste, alberi).
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Aggregate | Iterable<Documento> |
dichiara iterator() |
| ConcreteAggregate | Scaffale |
contiene i dati (qui un array) e crea l'iteratore |
| Iterator | java.util.Iterator<Documento> |
hasNext(), next() |
| ConcreteIterator | la classe anonima in iterator() e quella di soloDisponibili() |
tiene la posizione di scansione |
In Java il pattern è nel linguaggio: Iterable e Iterator, e il ciclo for (T x : aggregato) chiama iterator(). Gli esempi EsempioLista1 del corso usano ListIterator<String> su una ArrayList (con anche hasPrevious, add, set, remove). Il programma mostra un aggregato ad array, un secondo iteratore con filtro e il comportamento fail-fast.
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
class Documento {
final String titolo; final boolean disponibile;
Documento(String titolo, boolean disponibile) { this.titolo = titolo; this.disponibile = disponibile; }
@Override public String toString() { return titolo + (disponibile ? "" : " (in prestito)"); }
}
/** Aggregate: espone un Iterator e non rivela come i documenti sono memorizzati (qui: array). */
class Scaffale implements Iterable<Documento> {
private Documento[] dati = new Documento[2];
private int n = 0;
private int modifiche = 0; // serve all'iteratore fail-fast
void aggiungi(Documento d) {
if (n == dati.length) dati = java.util.Arrays.copyOf(dati, n * 2);
dati[n++] = d;
modifiche++;
}
/** Iterator (classe interna: vede lo stato dell'aggregato). */
@Override public Iterator<Documento> iterator() {
return new Iterator<Documento>() {
private int pos = 0;
private final int attese = modifiche;
public boolean hasNext() { return pos < n; }
public Documento next() {
if (attese != modifiche) throw new ConcurrentModificationException();
if (!hasNext()) throw new NoSuchElementException();
return dati[pos++];
}
};
}
/** Un secondo modo di scorrere lo stesso aggregato: solo i documenti disponibili. */
Iterable<Documento> soloDisponibili() {
return () -> new Iterator<Documento>() {
private final Iterator<Documento> it = iterator();
private Documento prossimo = avanza();
private Documento avanza() {
while (it.hasNext()) { Documento d = it.next(); if (d.disponibile) return d; }
return null;
}
public boolean hasNext() { return prossimo != null; }
public Documento next() {
if (prossimo == null) throw new NoSuchElementException();
Documento r = prossimo; prossimo = avanza(); return r;
}
};
}
}
public class IteratorEx {
public static void main(String[] args) {
Scaffale s = new Scaffale();
s.aggiungi(new Documento("Il nome della rosa", true));
s.aggiungi(new Documento("Il pendolo di Foucault", false));
s.aggiungi(new Documento("Baudolino", true));
for (Documento d : s) System.out.println("tutti: " + d); // il for-each usa iterator()
for (Documento d : s.soloDisponibili()) System.out.println("disponibili: " + d);
try {
for (Documento d : s) s.aggiungi(new Documento("Nuovo", true)); // modifica durante la scansione
} catch (ConcurrentModificationException e) {
System.out.println("ConcurrentModificationException: scaffale modificato durante la scansione");
}
}
}tutti: Il nome della rosa
tutti: Il pendolo di Foucault (in prestito)
tutti: Baudolino
disponibili: Il nome della rosa
disponibili: Baudolino
ConcurrentModificationException: scaffale modificato durante la scansioneL'iteratore fail-fast confronta un contatore delle modifiche (modifiche) con il valore visto alla creazione e lancia ConcurrentModificationException se l'aggregato è cambiato durante la scansione (stesso meccanismo delle collezioni del JDK). Distinguere: iteratore esterno (il client chiama next) o interno (si passa il codice all'aggregato: forEach, gli stream, vedi Architettura del software - decisioni, organizzazione e stiliL'architettura del software descrive come un sistema è organizzato in componenti, le loro relazioni e la loro distribuzione; è il ponte tra requisiti e progetto dettagliato, e le sue decisioni derivano soprattutto dai requisiti non funzionali (prestazioni, sicurezza, disponibilità, manutenibilità), spesso in conflitto tra loro; si descrive con più viste (logica, processi, sviluppo, fisica); gli stili di organizzazione ricorrenti sono a livelli (layered), MVC, repository, client-server, pipe and filter, publish-subscribe e microservizi; la modularizzazione mira ad alta coesione e basso accoppiamento, nascondendo le decisioni dietro interfacce (information hiding, inversione delle dipendenze).Architettura del software - decisioni, organizzazione e stili → per la pipeline di filtri).
Template Method
Intento. Definire lo scheletro di un algoritmo in un'operazione, rinviando alcuni passi alle sottoclassi: le sottoclassi ridefiniscono certi passi senza cambiare la struttura dell'algoritmo.
Quando. Si vuole realizzare una volta sola la parte invariante di un algoritmo e lasciare alle sottoclassi le parti che variano; si vuole evitare codice duplicato fra sottoclassi; si vuole controllare le estensioni (i soli punti di estensione sono i passi astratti e gli hook).
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| AbstractClass | ProceduraPrestito |
il template method esegui (final), i passi comuni, i passi astratti e l'hook |
| ConcreteClass | PrestitoLibro, PrestitoDvd |
realizzano i passi astratti, possono ridefinire i passi comuni o l'hook |
/** Template Method: lo scheletro dell'algoritmo e' fisso, i passi variabili sono metodi astratti o hook. */
abstract class ProceduraPrestito {
/** Il template method: final, nessuna sottoclasse puo' cambiare l'ordine dei passi. */
public final boolean esegui(String utente, String titolo) {
System.out.println("--- " + getClass().getSimpleName() + ": " + utente + " chiede '" + titolo + "'");
if (!verificaUtente(utente)) { System.out.println("utente non abilitato"); return false; }
if (!verificaDisponibilita(titolo)) { System.out.println("non disponibile"); return false; }
registra(utente, titolo); // passo astratto: ogni sottoclasse lo realizza
dopoRegistrazione(utente, titolo); // hook: facoltativo, per default non fa nulla
return true;
}
protected boolean verificaUtente(String utente) { return !utente.isBlank(); } // comune
protected abstract boolean verificaDisponibilita(String titolo);
protected abstract void registra(String utente, String titolo);
protected void dopoRegistrazione(String utente, String titolo) { } // hook
}
class PrestitoLibro extends ProceduraPrestito {
private int copie = 1;
@Override protected boolean verificaDisponibilita(String titolo) { return copie > 0; }
@Override protected void registra(String utente, String titolo) {
copie--;
System.out.println("libro registrato, scadenza tra 30 giorni");
}
}
class PrestitoDvd extends ProceduraPrestito {
@Override protected boolean verificaUtente(String utente) { // ridefinisce un passo comune
return super.verificaUtente(utente) && !utente.startsWith("minore");
}
@Override protected boolean verificaDisponibilita(String titolo) { return true; }
@Override protected void registra(String utente, String titolo) {
System.out.println("dvd registrato, scadenza tra 3 giorni");
}
@Override protected void dopoRegistrazione(String utente, String titolo) { // usa l'hook
System.out.println("promemoria SMS a " + utente);
}
}
public class TemplateEx {
public static void main(String[] args) {
ProceduraPrestito libro = new PrestitoLibro();
libro.esegui("anna", "Baudolino");
libro.esegui("luca", "Baudolino"); // nessuna copia
ProceduraPrestito dvd = new PrestitoDvd();
dvd.esegui("anna", "Metropolis");
dvd.esegui("minore-12", "Metropolis");
}
}--- PrestitoLibro: anna chiede 'Baudolino'
libro registrato, scadenza tra 30 giorni
--- PrestitoLibro: luca chiede 'Baudolino'
non disponibile
--- PrestitoDvd: anna chiede 'Metropolis'
dvd registrato, scadenza tra 3 giorni
promemoria SMS a anna
--- PrestitoDvd: minore-12 chiede 'Metropolis'
utente non abilitatoSi tratta del principio di Hollywood ("non chiamarci, ti chiamiamo noi"): è la superclasse a chiamare i metodi delle sottoclassi. Il Factory Method (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 →) è spesso un passo di un template method (PizzaStore.orderPizza chiama createPizza). Un hook è un metodo con comportamento di default (spesso vuoto) che le sottoclassi possono ridefinire; i passi astratti invece vanno obbligatoriamente realizzati.
Gli altri comportamentali
| pattern | idea | quando |
|---|---|---|
| State | l'oggetto cambia comportamento al cambiare del suo stato interno, delegando a un oggetto-stato | macchine a stati con molti if sullo stato (copia: disponibile, in prestito, prenotata, in restauro) |
| Visitor | aggiunge operazioni a una struttura eterogenea senza modificarne le classi (accept(visitor)) |
molte operazioni diverse su una struttura stabile di classi (per esempio un albero sintattico) |
| Chain of Responsibility | la richiesta scorre lungo una catena di gestori finché uno la tratta | filtri di richieste, gestione di eventi in cascata (approvazioni) |
| Mediator | un oggetto coordina le interazioni di un gruppo di colleghi, che non si conoscono tra loro | finestre di dialogo con controlli interdipendenti |
| Memento | salva e ripristina lo stato di un oggetto senza violarne l'incapsulamento | undo, punti di ripristino |
| Interpreter | rappresenta una grammatica e ne interpreta le frasi | linguaggi molto piccoli (espressioni, regole di ricerca) |
Domande d'esame
1. Spiegare l'Observer: ruoli, vantaggi e un problema che si incontra nell'implementazione.
Traccia. Subject con lista di Observer, che si registrano e cancellano; alla modifica il subject chiama aggiorna su ognuno; vantaggi: basso accoppiamento (il subject non conosce le classi concrete), osservatori aggiunti senza modificare il subject. Problemi: cancellarsi durante la notifica (ConcurrentModificationException, si risolve con una copia della lista), ordine di notifica, osservatori lenti o che lanciano eccezioni, notifiche a catena.
2. Strategy contro Template Method: stesso problema, quali differenze? Traccia. Entrambi rendono variabile un algoritmo. Strategy: composizione, l'algoritmo è un oggetto separato scambiabile a runtime, il Context lo usa. Template Method: ereditarietà, lo scheletro sta nella superclasse e i passi nelle sottoclassi, fissi alla creazione dell'oggetto. Strategy è più flessibile; Template Method evita di avere un oggetto in più e controlla i punti di estensione.
3. Come si realizza l'annullamento (undo) di un'operazione con Command?
Traccia. Ogni comando ha esegui() e annulla() (l'inverso, o il ripristino dello stato precedente con un Memento); l'Invoker esegue il comando e lo mette in una pila (storico); l'undo toglie il comando in cima e chiama annulla(). Un comando che fallisce non entra nello storico.
Versione ripasso
- Comportamentali: interazioni dinamiche e distribuzione delle responsabilità; undici: Observer, Strategy, Command, Iterator, Template Method, State, Visitor, Chain of Responsibility, Mediator, Memento, Interpreter.
| pattern | intento | ruoli |
|---|---|---|
| Observer (publish-subscribe) | i dipendenti sono avvisati quando il soggetto cambia | Subject, Observer, ConcreteSubject, ConcreteObserver |
| Strategy | famiglia di algoritmi intercambiabili | Strategy, ConcreteStrategy, Context |
| Command | la richiesta è un oggetto (coda, undo, log) | Command, ConcreteCommand, Receiver, Invoker, Client |
| Iterator | scorre un aggregato senza esporne la struttura | Aggregate, ConcreteAggregate, Iterator, ConcreteIterator |
| Template Method | scheletro fisso, passi alle sottoclassi | AbstractClass (template final, passi astratti, hook), ConcreteClass |
- Observer: il soggetto non conosce le classi degli osservatori;
registra/cancellaa runtime; push (dati nell'evento) o pull (passa se stesso).OsservatoImpldel corso:ConcurrentModificationExceptionse un osservatore si cancella nelladeliver; conCopyOnWriteArrayListsi itera su una copia. Il corso non impedisce la registrazione doppia.java.util.Observableè deprecato:PropertyChangeListener,Flow. L'MVC è un Observer fra modello e vista.
interface Osservatore<E> { void aggiorna(E evento); }
class Soggetto<E> {
private final List<Osservatore<E>> osservatori = new CopyOnWriteArrayList<>();
boolean registra(Osservatore<E> o) { return osservatori.add(o); }
boolean cancella(Osservatore<E> o) { return osservatori.remove(o); }
protected void notifica(E evento) { for (Osservatore<E> o : osservatori) o.aggiorna(evento); }
} Uscita: dopo registra(anna), registra(luca), registra(stat), cancella(luca) e una lambda, restituisci() avvisa anna e la lambda; restituzioni contate: 1, osservatori: 3.
Calculatordella dispensa:return 0;voluto per far fallire il test (red), poireturn sum;(green).- Strategy:
PoliticaMultaconMultaStudente(0,10 per giorno),MultaDocente(7 giorni di tolleranza, 0,20),MultaEsterno(0,50 per giorno, tetto 15,00); ilContext(Utente) la cambia a runtime; interfaccia a un metodo lambda (g -> 0). Strategy = composizione, Template Method = ereditarietà; State ha la stessa struttura ma cambia strategia da solo.
@FunctionalInterface interface PoliticaMulta { double calcola(int giorniRitardo); }
class Utente {
private PoliticaMulta politica;
void cambiaPolitica(PoliticaMulta p) { this.politica = p; }
}
ospite.cambiaPolitica(g -> 0); // amnistiaUscite: studente 10 giorni 1,00; docente 10 giorni 0,60; ospite 40 giorni 15,00 poi 0,00 dopo l'amnistia.
- Command:
esegui()/annulla(); l'Invoker (Sportello) tiene lo storico (pila) per l'undo; un comando che lancia non entra nello storico; macro-comandi (Composite), Memento per salvare lo stato. Esempio: due prestiti (copie 2 0), un terzo fallisce (nessuna copia di rosa), due undo riportano le copie a 2, un terzo undo stampaniente da annullare.
interface Comando { void esegui(); void annulla(); }
void esegui(Comando c) { c.esegui(); storico.push(c); } // se esegui() lancia, non si arriva al push
void annullaUltimo() { Comando c = storico.pop(); c.annulla(); }- Iterator: in Java è nel linguaggio (
Iterable,Iterator,for-each);ListIteratorha anchehasPrevious,add,set,remove. Più iteratori sullo stesso aggregato (tutti, solo i disponibili). Fail-fast: contatore delle modifiche confrontato a ogninext(),ConcurrentModificationException. Esterno (next) o interno (forEach, stream).
public Iterator<Documento> iterator() {
return new Iterator<Documento>() {
private int pos = 0;
private final int attese = modifiche; // contatore delle modifiche alla creazione
public boolean hasNext() { return pos < n; }
public Documento next() {
if (attese != modifiche) throw new ConcurrentModificationException(); // fail-fast
if (!hasNext()) throw new NoSuchElementException();
return dati[pos++];
}
};
} Uscita: for (Documento d : s) stampa i tre documenti; soloDisponibili() stampa solo Il nome della rosa e Baudolino; un aggiungi dentro il ciclo dà ConcurrentModificationException.
- Template Method:
eseguièfinal; passi comuni, astratti (obbligatori) e hook (default vuoto); principio di Hollywood; il Factory Method è spesso un passo. Esempio: libro con una copia: seconda richiestanon disponibile; DVD perminore-12:utente non abilitato.
public final boolean esegui(String utente, String titolo) {
if (!verificaUtente(utente)) return false;
if (!verificaDisponibilita(titolo)) return false; // astratto
registra(utente, titolo); // astratto
dopoRegistrazione(utente, titolo); // hook
return true;
}| esigenza | pattern |
|---|---|
| avvisare chi dipende da un cambiamento | Observer |
| scegliere un algoritmo, anche a runtime | Strategy |
| accodare, registrare o annullare azioni | Command |
| scorrere un aggregato senza conoscerlo | Iterator |
| scheletro fisso con passi variabili | Template Method |
| comportamento che dipende dallo stato | State |
| nuove operazioni su una struttura stabile | Visitor |
- Confronti utili: Observer e Mediator (uno-a-molti senza coordinamento contro coordinamento centrale di un gruppo); Strategy e State (struttura uguale, ma in State l'oggetto cambia strategia da solo); Command e Strategy (la richiesta come oggetto con
annullacontro l'algoritmo come oggetto); Iterator e Composite (spesso si scorre un albero di Composite con un Iterator). - Altri: State (comportamento dipende dallo stato), Visitor (operazioni su una struttura stabile), Chain of Responsibility, Mediator (coordina colleghi), Memento (undo), Interpreter (piccole grammatiche).
- Uscita di
TemplateEx:PrestitoLibroper anna:libro registrato, scadenza tra 30 giorni; per luca (nessuna copia)non disponibile;PrestitoDvdper anna:dvd registrato, scadenza tra 3 giorniepromemoria SMS a anna(hook); perminore-12:utente non abilitato(passo comune ridefinito). - Uscita di
StrategyEx/CommandEx: vedi sopra; inCommandExlo storico fa tornare le copie a 2 con dueannullaUltimo(). - Errori tipici: osservatori che si cancellano nella notifica su una lista normale;
ifsul tipo invece di Strategy; comando che modifica lo stato prima di sapere se avrà successo (undo incoerente); modificare l'aggregato durante il ciclofor-each; template method nonfinalche le sottoclassi riscrivono; confondere Strategy e State, Strategy e Template Method.