Esercizio - Observer per le prenotazioni dei documenti (progetto originale)
Esercizio originale, nello stile del progetto d'esame (scelta e applicazione di un pattern a un problema, con il codice). Teoria: Pattern comportamentali - Observer, Strategy, Command, Iterator e Template MethodI pattern comportamentali descrivono come gli oggetti collaborano e si dividono le responsabilità: Observer (publish-subscribe) avvisa automaticamente i dipendenti quando il soggetto cambia stato, mantenendo basso l'accoppiamento (si registrano e cancellano a runtime, modello push o pull, attenzione alle modifiche durante la notifica); Strategy rende intercambiabili gli algoritmi dietro un'interfaccia (in Java anche con una lambda); Command trasforma la richiesta in un oggetto, quindi memorizzabile, accodabile e annullabile (undo); Iterator scorre un aggregato senza esporne la struttura (for-each, fail-fast); Template Method fissa lo scheletro di un algoritmo e lascia alle sottoclassi passi astratti o hook (principio di Hollywood); State, Visitor, Chain of Responsibility, Mediator, Memento e Interpreter completano le undici forme del catalogo.Pattern comportamentali - Observer, Strategy, Command, Iterator e Template Method →, 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 →. Il codice è stato compilato ed eseguito con JDK 21.
Testo
Nel sistema della biblioteca, quando una copia di un documento torna disponibile devono succedere più cose:
- (a) gli iscritti che hanno prenotato il documento sono avvisati, nell'ordine in cui hanno prenotato;
- (b) un cruscotto dello sportello aggiorna il numero di copie disponibili;
- (c) in futuro si vorranno aggiungere altri tipi di avviso (SMS, statistiche) senza modificare la classe
Documento; - (d) un iscritto può disdire la prenotazione in qualsiasi momento, anche mentre il documento sta notificando;
- (e) un iscritto avvisato esce dalla lista delle prenotazioni.
Si richiede di: 1) scegliere il pattern e motivare la scelta confrontandola con almeno un'alternativa; 2) indicare i ruoli con le classi; 3) scrivere il codice Java completo con un main che mostri due restituzioni consecutive; 4) dire come si gestisce la disdetta durante la notifica e se il modello è push o pull.
Soluzione
1. Scelta del pattern
Il problema è una dipendenza uno-a-molti (un documento, tanti interessati) in cui l'oggetto che cambia non deve conoscere le classi di chi ascolta e il numero dei dipendenti varia a runtime: è l'Observer (publish-subscribe).
| alternativa | perché si scarta |
|---|---|
Documento chiama direttamente Utente.avvisa(), Cruscotto.aggiorna() |
accoppia Documento a ogni classe concreta: ogni nuovo avviso (punto c) richiede di modificarlo (viola aperto/chiuso) |
| il cruscotto interroga di continuo ogni documento (polling) | spreco di risorse, ritardo nella notifica, e non risolve l'ordine delle prenotazioni |
| Mediator | utile per coordinare gruppi di colleghi che si parlano tra loro; qui la comunicazione è a senso unico |
2. Ruoli
| ruolo | classe |
|---|---|
| Subject / ConcreteSubject | Documento (tiene la lista, prenota, disdici, restituisci) |
| Observer | OsservatoreDocumento (interfaccia con copiaDisponibile(Documento)) |
| ConcreteObserver | UtentePrenotato (avviso all'utente), Cruscotto (aggiorna il numero di copie) |
3. Codice
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/** Observer (modello "pull"): l'evento porta solo il soggetto, gli osservatori leggono cio' che gli serve. */
interface OsservatoreDocumento {
void copiaDisponibile(Documento d);
}
/** Subject. Non conosce nessuna classe concreta di osservatore. */
class Documento {
private final String titolo;
private int copieDisponibili;
// l'ordine di registrazione e' l'ordine di prenotazione; la lista consente (de)registrazioni durante la notifica
private final List<OsservatoreDocumento> osservatori = new CopyOnWriteArrayList<>();
Documento(String titolo, int copie) { this.titolo = titolo; this.copieDisponibili = copie; }
String titolo() { return titolo; }
int copieDisponibili() { return copieDisponibili; }
void prenota(OsservatoreDocumento o) { osservatori.add(o); }
void disdici(OsservatoreDocumento o) { osservatori.remove(o); }
void prendi() {
if (copieDisponibili == 0) throw new IllegalStateException("nessuna copia");
copieDisponibili--;
}
void restituisci() {
copieDisponibili++;
for (OsservatoreDocumento o : osservatori) o.copiaDisponibile(this); // notifica in ordine di prenotazione
}
}
/** ConcreteObserver 1: avviso per l'utente, che dopo l'avviso esce dalla lista. */
class UtentePrenotato implements OsservatoreDocumento {
private final String nome;
UtentePrenotato(String nome) { this.nome = nome; }
public void copiaDisponibile(Documento d) {
System.out.println("avviso a " + nome + ": '" + d.titolo() + "' ritirabile entro 48 ore");
d.disdici(this); // si puo' fare mentre si sta notificando: CopyOnWriteArrayList
}
}
/** ConcreteObserver 2: il cruscotto del banco, aggiunto senza modificare Documento. */
class Cruscotto implements OsservatoreDocumento {
public void copiaDisponibile(Documento d) {
System.out.println("cruscotto: " + d.titolo() + " -> " + d.copieDisponibili() + " copie disponibili");
}
}
public class Prenotazioni {
public static void main(String[] args) {
Documento rosa = new Documento("Il nome della rosa", 1);
rosa.prendi(); // ora e' tutto in prestito
UtentePrenotato anna = new UtentePrenotato("anna");
UtentePrenotato luca = new UtentePrenotato("luca");
UtentePrenotato piero = new UtentePrenotato("piero");
rosa.prenota(anna);
rosa.prenota(luca);
rosa.prenota(piero);
rosa.prenota(new Cruscotto());
rosa.disdici(luca); // luca disdice
rosa.restituisci();
System.out.println("--- seconda restituzione (anna e piero erano gia' usciti dalla lista)");
rosa.prendi();
rosa.restituisci();
}
}Uscita:
avviso a anna: 'Il nome della rosa' ritirabile entro 48 ore
avviso a piero: 'Il nome della rosa' ritirabile entro 48 ore
cruscotto: Il nome della rosa -> 1 copie disponibili
--- seconda restituzione (anna e piero erano gia' usciti dalla lista)
cruscotto: Il nome della rosa -> 1 copie disponibiliLettura dell'uscita:
- La prima
restituisci()notifica, nell'ordine di prenotazione,annaepiero;lucaha disdetto e non compare (punto d). Poi il cruscotto. - Ogni
UtentePrenotatochiamad.disdici(this)dentrocopiaDisponibile: dopo l'avviso esce dalla lista (punto e). - Alla seconda restituzione sono rimasti registrati solo il cruscotto (non esce mai):
annaepierosono già usciti e non ricevono nulla.
4. Disdetta durante la notifica, push o pull
- Disdetta durante la notifica. Una lista normale (
LinkedList,ArrayList) lanciaConcurrentModificationExceptionse un osservatore si cancella mentrerestituiscila sta scorrendo (lo stesso difetto ha l'OsservatoImpldegli esempi del corso).CopyOnWriteArrayListitera su una fotografia della lista, quindidisdicidentro la notifica è sicuro, anche da un altro 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 costo è una copia a ogni modifica, accettabile perché le (de)registrazioni sono rare rispetto alle notifiche. L'ordine di inserimento è conservato, e questo realizza il punto (a). - Pull. L'evento passa solo
this(Documento) e ogni osservatore legge ciò che gli serve (titolo(),copieDisponibili()): il cruscotto usa le copie, l'utente il titolo; aggiungere un dato da leggere non cambia l'interfaccia. Nel push (CopiaRestituitacon i dati) l'interfaccia fissa i dati che si spediscono, e i dati non utili a un osservatore vengono comunque passati. Si sceglie il pull quando gli osservatori hanno esigenze diverse. - Altre scelte. Chi si avvisa per primo: l'ordine di registrazione. Se un osservatore lancia un'eccezione, gli altri non sono avvisati: in produzione si cattura dentro il ciclo. La classe
Documentoaggiornerebbe anche lo stato prima di notificare (copieDisponibili++è fatto prima del ciclo), così gli osservatori vedono uno stato coerente.
Versione ripasso
- Problema: dipendenza uno-a-molti; chi cambia non deve conoscere chi ascolta; i dipendenti variano a runtime Observer. Alternative scartate: chiamate dirette (accoppiamento, viola aperto/chiuso), polling (spreco, ritardo), Mediator (coordina colleghi).
- Ruoli: Subject =
Documento(lista,prenota,disdici,restituisci); Observer =OsservatoreDocumento; ConcreteObserver =UtentePrenotato,Cruscotto.
interface OsservatoreDocumento { void copiaDisponibile(Documento d); }
class Documento {
private final List<OsservatoreDocumento> osservatori = new CopyOnWriteArrayList<>();
void prenota(OsservatoreDocumento o) { osservatori.add(o); }
void disdici(OsservatoreDocumento o) { osservatori.remove(o); }
void restituisci() {
copieDisponibili++; // prima lo stato...
for (OsservatoreDocumento o : osservatori) o.copiaDisponibile(this); // ...poi la notifica
}
}
class UtentePrenotato implements OsservatoreDocumento {
public void copiaDisponibile(Documento d) {
System.out.println("avviso a " + nome + ": '" + d.titolo() + "' ritirabile entro 48 ore");
d.disdici(this); // esce dalla lista durante la notifica
}
}- Uscita: prima restituzione:
avviso a anna,avviso a piero(luca ha disdetto), poicruscotto: ... 1 copie disponibili; seconda restituzione: solo il cruscotto. - Disdetta durante la notifica: lista normale
ConcurrentModificationException;CopyOnWriteArrayListitera su una copia (sicuro anche tra thread), costo di copia a ogni modifica accettabile; ordine di inserimento = ordine di prenotazione. - Pull (passa
this, l'osservatore legge ciò che serve) contro push (dati nell'evento): pull con osservatori dalle esigenze diverse. Stato aggiornato prima di notificare. Eccezioni in un osservatore: catturarle nel ciclo. - Errori tipici:
Documentoche nomina le classi concrete; osservatori su lista non protetta che si cancellano nella notifica; notifica prima dell'aggiornamento dello stato; dimenticare di cancellare l'osservatore (perdita di memoria: il soggetto lo tiene vivo).