MokaByte Numero 26  -  Gennaio 98  

Patterns: Observer

di 
Lorenzo Bettini

Iniziamo a vedere qualche pattern: vediamo il pattern Observer, tra l'altro presente nella libreria di classi di Java


Le volte scorse si è parlato (a livello introduttivo) di Pattern [Bet98], [Tre98], [LoR98], [Gra98]; iniziamo a vedere in dettaglio qualche pattern: questo mese vedremo il pattern Observer, che tra l'altro è implementato fra le classi di Java.


Introduzione

Spesso essere in grado di gestire alcuni dati non è sufficiente: si può non essere i soli (intendendo i programmatori) a dovere modificare o creare tali dati; si può avere la necessità di far modificare i dati anche ad utenti esterni. Questo problema si presenta in praticamente tutti i software gestionalil, o comunque in qualsiasi programma che presuppone un minimo di interazione fra l'utente ed il programma.

Il primo esempio che viene in mente è quello di un programma che permette di editare certi documenti (siano questi di qualsiasi natura, come lettere, fogli di calcolo, archivi, ecc...).

Le regole della buona programmazione ci insegnano a separare concetti concettualmente differenti, e la programmazione ad oggetti ribadisce questo concetto promuovendo la riusabilità di codice; per ottenere questo (e molti altri vantaggi) è necessario non tenere legate classi che si occupano di compiti diversi, nonostante queste debbano continuamente comunicare fra di loro e scambiarsi informazioni.

E' quindi ragionevole pensare di avere codice e quindi classi diverse che si occupano di compiti diversi in un programma del tipo di cui si è parlato sopra: una parte del codice si preoccuperà di gestire i dati (spesso binari del documento), mentre una parte si occuperà di gestire le richieste dell'utente, e quindi di modificare lo stato interno del documento, seguendo le modifiche immesse dall'esterno. Si posso quindi distinguere due tipi di classi:

Queste due tipologie di classi devono continuamente comunicare ed accedere le une ai dati delle altre. Sono quindi classi strettamente legate, ma solo dalla necessità di comunicare e di sincronizzarsi. Sarebbe quindi auspicabile separarle concettualmente in modo da poter cambiare tranquillamente una o più classi di uno dei due tipi, senza dovere rimodificare le altre. Questo del resto non è un concetto nuovo della programmazione ad oggetti, come abbiamo avuto più volte occasione di ribadire.

Vediamo quindi una soluzione elegante, efficiente, e soprattuto riutilizzabile per esaudire tutte queste richieste in modo semplice ed il più possibile indolore.

Observer

Chi ha già programmato sotto Windows utilizzando le MFC, e soprattutto i suoi wizard si sarà senz'altro trovato di fronte alla filosofia del Document/View, tramite la quale si separa la gestione interna del documento dalla visione (vista) che se ne dà all'esterno. Questo concetto era presente anche nelle primissime versioni della libreria Turbo Vision (presente nelle versioni precedenti alla 4 dei compilatori Borland), per creare interfacce in modalità testo.

Del resto si supponga di utilizzare un programma di foglio elettronico: la parte principale del foglio sono senz'altro i dati scritti in forma tabellare; ma la praticità di un foglio elettronico non si esaurisce qui: è possibile specificare il contenuto di certe celle in funzione di altre, cioè come risultato di un'operazione aritmetica su una o più celle. Inoltre è anche possibile creare dei grafici che rappresentano in svariate forme (istogrammi, torte, grafici a tre dimensioni) i dati inseriti nelle tabelle. Una delle cose più affascinanti di un foglio elettronico è la sincronizzazione automatica fra tutte queste parti: aggiornando i valori di certe celle si provoca il ricalcolo automatico delle celle a queste collegate, ed inoltre anche il ridisegno dei grafici che rapprensentano questi dati. Ovviamente non è possibile conoscere a priori le varie parti che devono mantenersi sincronizzate e quindi si deve creare un meccanismo che mantenga queste consistenze a run time, ed inoltre che sia il più personalizzabile e riutilizzabile possibile.

Una prima soluzione potrebbe essere quella di far sì che periodicamente le varie interfacce-viste (per gli utenti) interroghino i dati per capire se vi sono stati cambiamenti e quindi se si debba ridisegnare alcune parti; quindi una sorta di meccanismo di polling. Non serve molto per capire che non si tratta di una soluzione efficiente, soprattutto se queste viste sono molte, ed i dati numerosi.

Conviene invece agire nel senso opposto: saranno i dati, che quando subiranno una certa modifica automaticamente manderanno la notifica di avvenuto cambiamento a tutte le viste; a quel punto le viste, se sarà necessario, potranno sincronizzarsi coi nuovi dati, o con quelli modificati. In questo modo si ha un notevole decoupling e quindi una forte indipendenza (soprattutto a livello di codice) fra queste due parti del programma.

Questa è proprio la base del pattern Observer; ovviamente la nostra fonte principale rimane costantemente la bibbia dei pattern: [GOF95], e citando proprio da questa:

il pattern Observer definisce una dipendenza uno-a-molti fra oggetti in modo che quando un oggetto cambia stato, tutti gli altri oggetti dipendenti ne sono informati

I partecipanti (sempre per mantenere lo stesso gergo di [GOF95], sono:

Scendeno un po' più nei dettagli implementativi si tratta di due classi astratte, anzi, la classe osservatore è in realtà un'interfaccia. In questo modo, ci sarà un protocollo standard di comunicazione, che sarà l'unica cosa che i due partecipanti devono conoscere: per il resto potranno interagire classi completamente diverse e sconosciute le une alle altre.

Il soggetto si manterrà una lista di tutti gli osservatori che lo osservano, e quando lo riterrà opportuno (di solito dopo la modifica dei dati, o comunque una modifica considerevole) notificherà i vari osservatori di questo fatto, questi ultimi potranno allora interrogare il soggetto e ricavarne informazioni sufficienti per aggiornarsi e rimanere consistenti coi dati (ad esempio i vari grafici verranno ridisegnati, per rispecchiare le modifiche fatte ai dati tabellari).

Il seguente diagramma UML ([Tre98-2]) illustra meglio le relazioni e i partecipanti al pattern;

In questo modo si ottiene il decoupling richiesto e si riuscirà a gestire, in modo del tutto automatizzato, l'aggiunta di nuovi observer a run time (del resto i documenti e le viste sono elementi dinamici per loro stessa natura).

Un'idea di implementazione

Vediamo adesso un'idea di base dell'implementazione (per maggiori dettagli si veda l'articolo di Andrea Trentini):

observer è semplicemente un'interfaccia che dichiare il metodo update; tale metodo prende come argomento un subject, che utilizzerà per richiedergli informazioni sui dati.

// idea di implementazione di un observer

public interface Observer {
  public void update( Subject subject ) ;
}

Il subject è invece una classe che implementa certi metodi che potranno essere riutilizzati dalle classi che agiranno come subject (derivando da questa classe).

// idea di implementazione di un subject

public class Subject {
  protected Vector observers = new Vector() ;

  public void addObserver( Observer o ) {
    observers.addElement( o ) ;
  }

  public void removeObserver( Observer o ) {
    observers.removeElement( o ) ;
  }

  public void notify() {
    Enumeration e = observers.getElements() ;

    while ( e.hasMoreElements() ) {
      ((Observer)e.nextElement()).update( this ) ;
    }
  }
}

Come si può notare notify richiama il suddetto metodo update su tutti gli observer che si sono registrati presso il subject (chiamando il metodo addObserver).

Notare che sarà responsabilità del subject richiamare notify (eventualmente ridefinendola) al momento opportuno: poichè la notifica può "scatenare" diversi aggiornamenti, è bene che venga richiamata dopo che sono stati effettuati notevoli cambiamenti.

Conclusioni

In realtà Java mette a disposizione fra le sue classi la classe Observable (che corrisponde a subject) e l'interfaccia Observer; è quindi già possibile utilizzare questo pattern per i propri scopi, ovviamente implementando e modificando quello che si ritiene opportuno.

Andrea Trentini entrerè in maggiori dettagli per quanto riguarda l'implementazione.

A presto :-)
Lorenzo

Bibliografia