L’observer è uno tra i più semplici ed intuitivi design pattern di tipo comportamentale. Questo tipo di pattern è utilizzato quando si è interessati allo stato di un oggetto e si desidera ricevere una notifica ogni qualvolta si verifichi un cambiamento di tale stato. Un caso pratico è quello di uno shop online in cui più utenti vogliono ricevere una notifica non appena un determinato item ritorni in stok e disponibile per l’acquisto.
Da un punto di vista formale, in accordo con la definizione data dalla Gang Of Four, lo scopo del patter observer è quello di:
Definire una dipendenza uno-a-molti tra gli oggetti in modo che quando un oggetto cambia stato, tutti i suoi dipendenti vengono notificati e aggiornati automaticamente.
In altre parole il pattern specifica una comunicazione tra due tipologie di oggetti: gli osservatori, detti Observer, e gli osservabili, detti Subject. Un oggetto osservabile è quello che notificano agli osservatori i propri cambiamenti di stato. La struttura del pattern è mostrata nella figura seguente.
L’implementazione del pattern prevede che il Subject
mantenga una lista di tutti gli Observer
registrati. A tale scopo espone un metodo per la registrazione, register()
, ed uno per la cancellazione, unregister()
, degli degli osservatori. Questi ultimi, invece, espongono un metodo di aggiornamento, update()
, che sarà invocato dall’oggetto osservato ad ogni cambiamento del proprio stato. Tale evento è rappresentato nell’implementazione proposta dal metodo notifyObservers()
, per il quale proponiamo la seguente semplice implementazione:
1 2 3 4 5 |
public void notifyObservers() { for ( Observer observer : observers ) { observer.update(); } } |
Si noti che Java fornisce una piattaforma integrata per l’implementazione del pattern Observer tramite la classe java.util.Observable
e l’interfaccia java.util.Observer
. Tuttavia non è molto utilizzato perché l’implementazione del pattern è davvero molto semplice e spesso si preferisce non estendere la classe java.util.Observable
fornita dal linguaggio solo per l’implementazione del pattern Observer, in quanto java non fornisce l’ereditarietà multipla.
Esempio Pratico
Riprendiamo il caso pratico che abbiamo introdotto all’inizio del post, e consideriamo la situazione in cui n° consumatori siano in attesa che un determinato item ritorni disponibile in stock. Implementiamo quindi due classi: un ItemProducer
ed un ItemConsumer
. I termini producer e consumer non sono stati scelti a caso, il pattern infatti è anche noto come modello publish-subscribe.
La classe ItemProducer
, che implementa l’interfaccia Subject
, è così definita:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class ItemProducer implements Subject { private Item item; private List<Observer> observers = new ArrayList<>(); private List<Observer> unregistered = new ArrayList<>(); public ItemProducer(Item item) { this.item = item; } public void register(Observer observer) { synchronized (item) { observers.add( observer ); } } public void unregister(Observer observer) { synchronized (item) { unregistered.add( observer ); } } public void notifyObservers() { synchronized (item) { observers.removeAll( unregistered ); } Iterator<Observer> iterator = observers.iterator(); while( iterator.hasNext() ) { iterator.next().update(); } } ... } |
Notiamo innanzitutto che la casse utilizza l’oggetto Item
(osservato) per la sincronizzazione delle parti di codice che accedono alla lista degli observer. Inoltre si fa uso di una lista di appoggio per inserirvi tutti gli osservatori che si sono de-registrati. Questo al fine di evitare di avere una eccezione di ConcurrentModificationException
durante l’esecuzione del metodo notifyObservers()
per i motivi che vedremo tra breve. Ovviamente prima di poter eseguire le notifiche la lista degli observer è aggiornata rimuovendo gli osservatori de-registrati.
Aggiungiamo infine due metodi alla classe che sono specifici del dominio trattato. Il primo, addInStock()
, che aggiunge nuovi item al magazzino e quindi invoca il metodo notifyObservers()
per notificare ai consumatori che la quantità disponibile è stata aggiornata. Il secondo metodo, reserve()
, che consente ai consumer di riservarsi il numero di item richiesti e che restituisce il numero di item effettivamente rilasciati. Questi infatti saranno in generale pari al numero di item richiesti, ad eccezione del caso in cui il magazzino ne contenga meno.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class ItemProducer implements Subject { private Item item; private List<Observer> observers = new ArrayList<>(); private List<Observer> unregistered = new ArrayList<>(); ... public void addInStock( Long quantity ) { synchronized (item) { System.out.println( "Added " + quantity + " items in stock"); item.setQuantity( item.getQuantity() + quantity ); } notifyObservers(); } public Long reserve( Long quantity ) { Long reserved = quantity; synchronized (item) { if ( item.getQuantity() < quantity ) { reserved = item.getQuantity(); item.setQuantity( 0L ); } else { item.setQuantity( item.getQuantity() - quantity ); } } return reserved; } } |
Veniamo ora all’implementazione della classe ItemConsumer
, che estende l’interfaccia Observer
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class ItemConsumer implements Observer { private Long required; private ItemProducer producer; public ItemConsumer(Long required) { this.required = required; System.out.println( "Waiting to have " + required + " item available" ); } public void setProducer(ItemProducer producer) { this.producer = producer; producer.register( this ); } public void update() { Long reserved = producer.reserve( required ); if ( reserved == required ) { System.out.println( "Got all the requred items, unregistering me!" ); producer.unregister( this ); } else { required = required - reserved; System.out.println( "Got " + reserved + " items, still waiting for " + required + " items" ); } } } |
La classe è istanziata sul numero di item richiesti, parametro required
, e si registra come osservatore della classe producer che gli viene assegnato nel metodo addProducer()
. Nel metodo update()
, che ricordiamo è invocato dal producer per notificare un aggiornamento nello stock, si procede riservando item dal producer e, se sono sufficienti a coprire la richiesta, il consumer provvede a cancellare la propria registrazione come observer. E’ proprio tale cancellazione che potrebbe generare una ConcurrentModificationException
, perchè rimuove l’observer dalla lista degli osservatori mentre il producer li sta ciclando per eseguire la notifica.
Per completezza riportiamo una classe Main
che utilizza le due classi ed il relativo output generato.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public static void main(String[] args) { // Generate Item and Producer Item item = new Item( "XYZ", 0L ); ItemProducer producer = new ItemProducer(item); // Generate consumer ItemConsumer consumer1 = new ItemConsumer( 2L ); consumer1.setProducer( producer ); ItemConsumer consumer2 = new ItemConsumer( 8L ); consumer2.setProducer( producer ); ItemConsumer consumer3 = new ItemConsumer( 5L ); consumer3.setProducer( producer ); producer.addInStock( 3L ); producer.addInStock( 4L ); producer.addInStock( 3L ); producer.addInStock( 5L ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Waiting to have 2 item available Waiting to have 8 item available Waiting to have 5 item available Added 3 items in stock Got all the requred items, unregistering me! Got 1 items, still waiting for 7 items Got 0 items, still waiting for 5 items Added 4 items in stock Got 4 items, still waiting for 3 items Got 0 items, still waiting for 5 items Added 3 items in stock Got all the requred items, unregistering me! Got 0 items, still waiting for 5 items Added 5 items in stock Got all the requred items, unregistering me! |
Listener
Una ulteriore implementazione del pattern Observer è possibile utilizzando il concetto di Property Change Listener fornito dal linguaggio java ed implementato nel package java.beans
. Il componente che gestisce la lista dei listener (ovvero degli observer) è il PropertyChangeSupport
, che quindi deve essere istanziato nella classe ItemProducer
ed utilizzato per inviare le notifiche:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public class ItemProducer { private Item item; private PropertyChangeSupport support; public ItemProducer(Item item) { this.item = item; this.support = new PropertyChangeSupport(this); } public void register(PropertyChangeListener observer) { this.support.addPropertyChangeListener( observer ); } public void unregister(PropertyChangeListener observer) { this.support.removePropertyChangeListener( observer ); } public void notifyObservers( Long oldQuantity ) { PropertyChangeEvent pce = new PropertyChangeEvent( this, "quantity", oldQuantity, this.item.getQuantity() ); support.firePropertyChange( pce ); } public void addInStock( Long quantity ) { Long oldQuantity = item.getQuantity(); item.setQuantity( oldQuantity + quantity ); notifyObservers( oldQuantity ); } public Long reserve( Long quantity ) { Long reserved = quantity; if ( item.getQuantity() < quantity ) { reserved = item.getQuantity(); item.setQuantity( 0L ); } else { item.setQuantity( item.getQuantity() - quantity ); } return reserved; } } |
La cosa più evidente di tale implementazione è che non sono più presenti porzioni di codice synchronized, in quanto non più necessari, e neppure la gestione della lista degli observer rimossi.
Per quanto riguarda invece l’implementazione dell’ItemConsume
, non ci sono molte modifiche da apportare ad eccezione del fatto che la classe deve implementare l’interfaccia java.beans.PropertyChangeListener
ed implementare il metodo propertyChange()
, corrispondente al metodo update()
del pattern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class ItemConsumer implements PropertyChangeListener { private Long required; private ItemProducer producer; public ItemConsumer(Long required) { this.required = required; System.out.println( "Waiting to have " + required + " item available" ); } public void setProducer(ItemProducer producer) { this.producer = producer; producer.register( this ); } public void propertyChange(PropertyChangeEvent pce) { Long reserved = producer.reserve( required ); if ( reserved == required ) { System.out.println( "Got all the requred items, unregistering me!" ); producer.unregister( this ); } else { required = required - reserved; System.out.println( "Got " + reserved + " items, still waiting for " + required + " items" ); } } } |
Codice Sorgente
Il codice sorgente con l’esempio presentato è scaricabile qui observer.