Observer

L’observer è un pattern comportamentale il cui scopo è quello di permettere ad un gruppo di oggetti (detti osservatori – Observer), di rilevare i cambiamenti di stato che avvengono in uno specifico oggetto (detto soggetto osservato – Subject), da loro tenuto sotto controllo.

Il pattern observer permette di realizzare una connessione lasca tra oggetti diversi, i quali, devono conoscersi il meno possibile ma devono riuscire a scambiarsi informazioni. Lavorare in polling non è quasi mai una buona idea, i soggetti osservati possono passare gran parte del loro tempo a rispondere negativamente alle verifiche di cambiamento di stato mosse dagli osservatori, piuttosto che a eseguire questi stessi cambiamenti. Per questo motivo, ad esempio, nei sistemi operativi sono stati sviluppati gli interrupt (per maggiori informazioni fare riferimento a: Input/Output nei Sistemi Operativi).

Il procedimento utilizzato dal pattern observer può essere così riassunto: gli osservatori devono potersi registrare all’oggetto osservato, il quale potrà accedere in un secondo momento alla lista contenente i riferimenti ai suoi osservatori e contattarli in caso di cambiamento di stato. Il pattern observer prevede due distinte modalità di funzionamento, identificate come “approccio push” e “approccio pull”, iniziamo a descrivere il funzionamento del più semplice approccio push.

Classi astratte coinvolte

Il pattern observer impiega le seguenti classi e interfacce:

  • Subject;
  • ConcreteSubject;
  • Observer;
  • ConcreteObserver.

L’interfaccia Subject definisce i metodi che dovranno essere implementati dai soggetti sotto osservazione. Tali metodi permettono la registrazione e la rimozione degli osservatori dalla lista di osservazione. I metodi promessi sono:

  • registerObserver(), inserisce un nuovo osservatore nella lista;
  • removeObserver(), elimina un osservatore dalla lista;
  • notifyObserver(), notifica a tutti gli osservatori che è avvenuto un cambiamento di stato.

L’interfaccia Observer definisce i metodi che dovranno essere implementati dagli oggetti osservatori. I metodi promessi sono:

  • update(), viene utilizzato dal ConcreteSubject per notificare l’avvenuto cambiamento di stato.

Le classi concrete ConcreteSubject e ConcreteObserver fanno riferimento rispettivamente ai soggetti sotto osservazione e agli oggetti osservati. Esse implementano le rispettive interfacce e quindi i relativi metodi.

Schema e relazioni

Le interfacce Subject e Observer sono poste in relazione di aggregazione, l’interfaccia Observer è infatti contenuta all’interno dell’interfaccia Subject. Un soggetto è normalmente osservato da almeno un osservatore, tuttavia può capitare che per un certo periodo di tempo un soggetto non venga osservato da nessuno. Allo stesso tempo un osservatore può osservare simultaneamente uno o più oggetti, eseguendo più istanze del medesimo pattern Observer. Le classi concrete dipendono dalla realizzazione delle relative interfacce, per questo motivo sono poste in relazione di dipendenza.

All’interno della classe ConcreteSubject è presente una struttura dati di tipo lista, il cui scopo è quello di contenere i riferimenti alle istanze di tipo Observer che si sono registrate ad essa come osservatori. La registrazione e la rimozione di un osservatore avvengono mediante l’utilizzo dei metodi pubblici registerObserver() e removeObserver().

Il metodo notifyObserver() viene richiamato dal soggetto sotto osservazione in seguito alla modifica del suo stato, tale metodo eseguirà la chiamata al metodo update() contenuto all’interno di tutte le istanze Observer registrare nella lista di osservazione. Le funzionalità di questo metodo possono variare sulla base delle specifiche della classe che lo definisce (collegamento dinamico).

Esempio d’uso

Consideriamo un esempio d’uso: una centralina metereologica collegata ad una piattaforma di controllo. Il WeatherDataObject (il soggetto sotto osservazione) è contenuto all’interno della centralina metereologica e avrà il compito di verificare lo stato dei vari sensori mediante un processo di polling, i diversi device (gli osservatori) demanderanno a lui il compito di controllare le eventuali variazioni di temperatura, umidità e pressione, in questo modo verrà svolto un unico controllo in polling al posto di tanti quanti sono gli osservatori.

Di seguito viene presentano un frammento di codice Java rappresentante il TemplateMethods, ovvero la “parte fredda” ereditata dal pattern e utilizzabile in tutti gli observer.

/* aggiungo alla lista l'osservatore */
public void registerObserver(Observer o) {
  observers.add(0);
}

/* rimuovo dalla lista l'osservatore */
public void removeObserver(Observer o) {
  int i = observer.indexOf(o);
  if(i >= 0)
    observers.remove(i);
}

/* scandisco gli observers e invoco il metodo update */
public void notifyObservers() {
  for(Observer o : observers)
    o.update(data); // passaggio dello stato
}

Nell’esempio sovrastante la variabile “observers” rappresenta la lista degli oggetti di tipo Observer che si sono registrati alla classe Subject, mentre la variabile “o” rappresenta una singola istanza di tipo Observer.

Approccio pull

La precedente definizione del metodo notifyObservers() presenta un imprecisione, esso è stato sviluppato per favorire l’approccio push, il quale prevede l’invio diretto dei dati dal soggetto all’osservatore, tuttavia tale approccio necessita di una customizzazione del metodo update() e come tale è in contrasto con la definizione di “parte fredda”, la quale dovrebbe essere il più generale possibile.

In generale è preferibile affidare all’osservatore il compito di interrogare il soggetto a seguito della notifica dell’avvenuto cambiamento di stato, l’osservatore potrà utilizzare il metodo pubblico getState() previsto dal soggetto e in questo modo acquisire le informazioni desiderate in forma più complessa. L’utilizzo dell’approccio pull richiede la ridefinizione parziale dello schema dell’architettura e l’aggiunta di un nuovo metodo alla classe ConcreteSubject. Di seguito viene indicato il nuovo schema dell’applicazione, le entità coinvolte restano le precedenti classi e interfacce: Subject, ConcreteSubject, Observer, ConcreteObserver.

L’interfaccia Subject definisce nuovamente i metodi che dovranno essere implementati dai soggetti sotto osservazione. I metodi promessi sono:

  • registerObserver(), inserisce un nuovo osservatore nella lista;
  • removeObserver(), elimina un osservatore dalla lista;
  • notifyObserver(), notifica a tutti gli osservatori che è avvenuto un cambiamento di stato.

L’interfaccia Observer definisce i metodi che dovranno essere implementati dagli oggetti osservatori. I metodi promessi sono:

  • update(), viene utilizzato dal ConcreteSubject per notificare il cambiamento di stato.

La classe ConcreteSubject rappresenta un soggetto concreto sotto osservazione, essa implementa tutti i metodi promessi dalla relativa interfaccia, in più mette a disposizione il seguente metodo pubblico:

  • getState(), restituisce lo stato del soggetto;

La classe ConcreteObserver rappresenta un osservatore, essa implementa il metodo pubblico update() promesso dall’interfaccia.

Nel nuovo schema rappresentate il diagramma delle classe del pattern Observer è stato introdotto il nuovo metodo getState() all’interno della classe ConcreteSubject, inoltre è stata aggiunta una relazione di dipendenza tra le classi ConcreteSubject e ConcreteObserver. Tale relazione è necessaria dal momento che l’osservatore dovrà utilizzare gli attributi o i metodi del soggetto osservato per risalire al suo nuovo stato.

Utilizzando l’approccio appena descritto sarà possibile ridurre il codice del metodo notifyObservers() alle seguenti righe di codice:

/* scandisco gli observers e invoco il metodo update */
public void notifyObservers() {
  for(Observer o : observers)
    o.update(); // comunicazione cambiamento di stato appena avvenuto
}