2.2 Introduzione al framework
2.2.6 Esecuzione differita
Ci sono categorie di operazioni che, se eseguite direttamente o indirettamente durante la gestione di un evento, possono produrre bug di difficile comprensione ed individuazione. Per chiarire il problema, di seguito vengono illustrati due possibili scenari in cui questo accade.
Esempio 1
Supponiamo di avere un oggetto di tipo IFMouse (chiamato myMouse) e due listener per l’evento LeftClick (chiamati listener1 e listener2 ). Il primo listener, una volta ricevuto l’evento ed in presenza di opportune condizioni, esegue un cambiamento di stato. Il se- condo invece, per svolgere il compito a lui assegnato, deve interrogare lo stato del mouse. Come spiegato in precedenza, listener2 non possiede un riferimento myMouse ma può procurarselo attraverso il metodo GetEventsSourceById della classe IFApplication. Se listener1 riceve per primo l’evento (ovvero se listener1 è stato registrato prima di liste- ner2 ), lo stato dell’applicazione cambierà prima che l’evento venga notificato a listener2. Segue che listener2 non riuscirà ad ottenere il riferimento a myMouse.
Si tratta di un caso di dipendenza logica tra i due listener. Ovviamente in questo caso il problema potrebbe essere risolto invertendo l’ordine di registrazione, tuttavia è auspicabile trovare una soluzione più generale e affidabile.
Esempio 2
Supponiamo di avere un oggetto di tipo IFMouse e quattro listener per l’evento LeftClick. Il listener2, una volta ricevuto l’evento ed in presenza di opportune condizioni, svolge i compiti a lui assegnati, si de-registra, e termina il suo ciclo di vita. Le operazioni eseguite dagli altri listener non sono significative per questo esempio. La classe IFMouse inserisce
i riferimenti ai consumatori in un array dinamico. La notifica di un evento consiste in un ciclo for nel quale si scorre l’array invocando il metodo OnLeftClick su ogni elemento. Terminata la gestione dell’evento da parte di listener2, il contatore del ciclo for viene incrementato assumendo come valore 2. Secondo le aspettative del programmatore, tale indice corrisponde a listener3. Tuttavia, a causa della de-registrazione di listener2, quel- l’elemento dell’array è ora occupato da listener4. Segue che listener3 non verrà notificato del verificarsi dell’evento.
Questo è uno dei classici bug dovuti alla modifica di una collezione di oggetti mentre si sta iterando su di essa.
In generale, questi problemi possono verificarsi quando, una volta terminata la fa- se di inizializzazione, viene eseguito un comando appartenente ad una delle seguenti categorie:
• Aggiunta o rimozione di listener, compresi i metodi Register()11 e Unregister().
• Creazione o rimozione di uno stato. • Cambiamento di stato.
• Creazione o rimozione di una modalità di interazione. • Cambiamento della modalità di interazione.
Una possibile soluzione del problema consiste nel differire l’esecuzione di questi coman- di al termine della fase di gestione degli eventi. Dal punto di vista implementativo, è possibile realizzare questa funzionalità sfruttando il pattern Command Processor [5], che consente di separate la richiesta di un servizio dalla sua esecuzione. Essendo basato sul pattern Command [16], anche questo pattern prevede che le richieste per determinati ser- vizi vengano incapsulate all’interno di oggetti, detti “command”. La principale differenza tra i due deriva però dalla presenza di un oggetto “command processor” incaricato dello scheduling e dell’esecuzione delle richieste. I partecipanti previsti dal pattern sono: Abstract Command - Classe astratta nella quale viene definita l’interfaccia per l’ese-
cuzione dei “command”.
Command - Rientrano in questa categoria tutte le classi che estendono AbstractCom- mand. Ognuna di queste classi rappresenta una specifica richiesta ed implementa i metodi necessari alla sua esecuzione.
Command Processor - Gestisce una collezione di oggetti “command” e ne attiva l’ese- cuzione secondo le politiche stabilite dalla classe stessa.
Controller - In risposta ad uno “stimolo” esterno istanzia un opportuno oggetto “com- mand” e ne delega l’esecuzione al “command processor”. Dal punto di vista im- plementativo lo “stimolo” altro non è che l’invocazione di un suo metodo, anche se concettualmente esso può rappresentare la richiesta di un servizio da parte di un’altra componente dell’applicazione oppure la ricezione di una notifica di qualche tipo (come avviene in questo framework).
Figura 2.2: Il pattern Command Processor
Vediamo ora come sono stati distribuiti questi ruoli tra le classi del framework.
La classe AbstractCommand non è stata definita esplicitamente, tuttavia essa consiste nel solo metodo Execute(), che deve essere implementato da tutte le classi “command”. In genere queste classi vengono definite dal programmatore in funzione delle specifiche necessità. Nel caso in cui tutto quello che si vuole fare consiste nell’invocazione di un singolo metodo, è possibile usare la classe di utilità IFCommand. L’interfaccia di tale classe è costituita da un costruttore e dal metodo Execute. Quest’ultimo si limita ad invocare un certo metodo su un dato oggetto, passando gli opportuni parametri. Come è facile intuire, il costruttore prende come parametro un riferimento all’oggetto sul quale invocare il metodo, il nome del metodo stesso (come stringa) ed una lista di parametri da passare al metodo.
Il ruolo di “command processor” è svolto dalla classe IFApplication. Essa gestisce una coda di “command” e fornisce il metodo InvokeLater per aggiungere un comando alla coda. L’esecuzione dei comandi consiste semplicemente in un ciclo nel quale, ad ogni iterazione, viene rimosso l’elemento in testa alla coda e viene invocato il metodo Execute su di esso. Come accennato in precedenza, l’esecuzione di questi comandi deve avvenire periodicamente e al termine della fase di gestione degli eventi. In XVR questo si traduce nell’ultima parte del ciclo OnFrame(). Si noti infine che, per evitare bug come quello illustrato nell’esempio 1, la coda è unica e indipendente da stati e modalità di interazione. Quanto ai “controller”, come si può intuire, si tratta di un sottoinsieme degli oggetti controller descritti nel paragrafo 2.2.1. Alla ricezione di un dato evento, essi creano un
oggetto “command” di tipo appropriato e ne delegano l’esecuzione alla classe IFApplication per mezzo del metodo InvokeLater.
Il seguente diagramma di attività fornisce un esempio di quanto detto: