Salta al contenuto
Note per Studenti Pattern comportamentali - Observer, Strategy, Command, Iterator e Template Method

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

Punti di progetto.

Nota sul codice del corso. Il file Calculator della dispensa, usato come primo passo del ciclo di sviluppo guidato dai test, termina con return 0; invece di return 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
java
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 euro

Con 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
java
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 annullare

Un 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.

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

L'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
java
/** 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 abilitato

Si 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/cancella a runtime; push (dati nell'evento) o pull (passa se stesso). OsservatoImpl del corso: ConcurrentModificationException se un osservatore si cancella nella deliver; con CopyOnWriteArrayList si 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.
java
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.

  • Calculator della dispensa: return 0; voluto per far fallire il test (red), poi return sum; (green).
  • Strategy: PoliticaMulta con MultaStudente (0,10 per giorno), MultaDocente (7 giorni di tolleranza, 0,20), MultaEsterno (0,50 per giorno, tetto 15,00); il Context (Utente) la cambia a runtime; interfaccia a un metodo →\to lambda (g -> 0). Strategy = composizione, Template Method = ereditarietà; State ha la stessa struttura ma cambia strategia da solo.
java
@FunctionalInterface interface PoliticaMulta { double calcola(int giorniRitardo); }
class Utente {
    private PoliticaMulta politica;
    void cambiaPolitica(PoliticaMulta p) { this.politica = p; }
}
ospite.cambiaPolitica(g -> 0);                 // amnistia

Uscite: 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 →\to 0), un terzo fallisce (nessuna copia di rosa), due undo riportano le copie a 2, un terzo undo stampa niente da annullare.
java
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); ListIterator ha anche hasPrevious, add, set, remove. Più iteratori sullo stesso aggregato (tutti, solo i disponibili). Fail-fast: contatore delle modifiche confrontato a ogni next(), ConcurrentModificationException. Esterno (next) o interno (forEach, stream).
java
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 richiesta non disponibile; DVD per minore-12: utente non abilitato.
java
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 annulla contro 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: PrestitoLibro per anna: libro registrato, scadenza tra 30 giorni; per luca (nessuna copia) non disponibile; PrestitoDvd per anna: dvd registrato, scadenza tra 3 giorni e promemoria SMS a anna (hook); per minore-12: utente non abilitato (passo comune ridefinito).
  • Uscita di StrategyEx/CommandEx: vedi sopra; in CommandEx lo storico fa tornare le copie a 2 con due annullaUltimo().
  • Errori tipici: osservatori che si cancellano nella notifica su una lista normale; if sul tipo invece di Strategy; comando che modifica lo stato prima di sapere se avrà successo (undo incoerente); modificare l'aggregato durante il ciclo for-each; template method non final che le sottoclassi riscrivono; confondere Strategy e State, Strategy e Template Method.

Esercizi su questo argomento

Teoria collegata