Salta al contenuto
Note per Studenti Esercizio - Observer per le prenotazioni dei documenti (progetto originale)

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

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

Lettura dell'uscita:

  1. La prima restituisci() notifica, nell'ordine di prenotazione, anna e piero; luca ha disdetto e non compare (punto d). Poi il cruscotto.
  2. Ogni UtentePrenotato chiama d.disdici(this) dentro copiaDisponibile: dopo l'avviso esce dalla lista (punto e).
  3. Alla seconda restituzione sono rimasti registrati solo il cruscotto (non esce mai): anna e piero sono già usciti e non ricevono nulla.

4. Disdetta durante la notifica, push o pull

Versione ripasso

  • Problema: dipendenza uno-a-molti; chi cambia non deve conoscere chi ascolta; i dipendenti variano a runtime →\to 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.
java
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), poi cruscotto: ... 1 copie disponibili; seconda restituzione: solo il cruscotto.
  • Disdetta durante la notifica: lista normale →\to ConcurrentModificationException; CopyOnWriteArrayList itera 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: Documento che 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).

Teoria collegata