2.2 Introduzione al framework
2.2.5 Gestione degli eventi
Uno degli obiettivi del framework consiste nel fornire i meccanismi e le astrazioni necessari ad implementare l’interazione utente in modo semplice e coerente. Particolare attenzione è stata quindi posta al fine di individuare un design che garantisse una gestione uniforme
dell’input (proveniente da dispositivi di varia natura) e delle notifiche interne all’applica- zione stessa. Il paradigma Event-driven ed i pattern ad esso correlati si sono rivelati la soluzione ideale.
Uno dei pattern più usati in questo paradigma è l’Observer, detto anche Publish-Subscribe o Delegation Event Model7 [16,23]. Tale pattern prevede:
• Una sorgente di eventi (detta anche publisher)
• Dei consumatori di eventi8 (detti anche listener, observer o subscriber)
• Un’interfaccia che specifica la modalità attraverso cui la sorgente notifica l’evento ai consumatori.
La sorgente definisce gli eventi da essa prodotti e gestisce una struttura dati contenente i riferimenti ai listener (tipicamente un array dinamico o una lista concatenata), fornendo i metodi per l’aggiunta e la rimozione dei listener. La generazione di un evento (che in genere corrisponde ad una variazione dello stato interno della sorgente), e quindi la notifica dello stesso, consiste nell’invocazione di un dato metodo su tutti i consumatori registrati. Per tale ragione tutti i consumatori devono implementare un’interfaccia nella quale vengono dichiarati i metodi (detti event handler) invocati dalla sorgente al verificarsi dei rispettivi eventi.
Il seguente diagramma delle classi illustra quanto detto:
7Questo nome è stata introdotto dalla Sun Microsystems con la release 1.1
di Java, nella quale è stato adottato un nuovo modello di gestione degli even-
ti per l’Abstract Window Toolkit (AWT). Per maggiori dettagli si visiti la pagina http://docs.oracle.com/javase/1.4.2/docs/guide/awt/1.3/designspec/events.html
8Tipicamente sono oggetti “controller”, ma non necessariamente: gli oggetti che astraggono dispositivi
di input, ad esempio, sono sia sorgenti che consumatori di eventi. Il loro compito è infatti quello di convertire gli eventi ricevuti da IFApplication in eventi di livello più alto.
Figura 2.1: Il pattern Observer
Nel framework le interfacce dei listener vengono specificate nella documentazione relativa alle sorgenti. Questo è dovuto al fatto che il linguaggio S3D non supporta tale costrutto. In alternativa si sarebbero potute definire delle classi “Adapter”9, ovvero classi che forniscono
un’implementazione di default (vuota) di ogni event handler, e che devono essere “sotto- classate” da ogni listener. Tuttavia si è preferito adottare un approccio diverso, nel quale i listener di ogni evento vengono gestiti separatamente10. Segue che ogni listener si registra
presso la sorgente per i soli eventi a cui è interessato e di conseguenza solo quelli gli verranno notificati. Questo approccio ha il vantaggio di minimizzare il numero di chiamate di metodo, in quanto ad ogni oggetto vengono notificati i soli eventi che può (e vuole) effettivamente gestire.
Dato un evento Evt, i nomi metodi per la gestione dei listener ed i nomi degli event handler seguono la seguente convenzione:
• Il metodo per la registrazione di un listener ha una firma del tipo: AddEvtListener(listener)
• Il metodo per la de-registrazione di un listener ha una firma del tipo: RemoveEvtListener(listener)
• Il metodo di gestione dell’evento deve avere una delle seguenti firme:
9Come avviene nelle librerie AWT e Swing di Java.
– OnEvt()
– OnEvt(descriptor) - dove descriptor è un descrittore contenente informazioni aggiuntive sull’evento
Consideriamo ora il seguente esempio, in cui un oggetto di tipo IFMouse viene registrato presso IFApplication per ricevere gli eventi di cui necessita:
// Creo un oggetto di tipo IFMouse.
// Il primo parametro del costruttore è l’id dell’oggetto ,
// il secondo verrà discusso a breve e può essere ignorato per ora.
var mouse = IFMouse ("mouse", f a l s e) ;
// Registrazione del mouse presso IFApplication
IFAPP . A d d P o s t R e n d e r i n g L i s t e n e r ( mouse ) ; IFAPP . A d d S y s t e m E v e n t F i r e d L i s t e n e r ( mouse ) ;
Questo codice è perfettamente valido, tuttavia è abbastanza tedioso per il programmatore dover consultare la documentazione per scoprire gli eventi di cui necessita IFMouse. Inol- tre il dimenticarsi anche di una sola registrazione produce bug di difficile individuazione. Per tale ragione ogni oggetto che deve “mettersi in ascolto” di uno o più eventi deve fornire due metodi, Register() e Unregister(), che provvedono ad effettuare tutte le registrazioni / de-registrazioni necessarie. Il codice precedente può allora essere riscritto come segue:
var mouse = IFMouse ("mouse", f a l s e) ; mouse . R e g i s t e r ( ) ;
Poiché è molto comune registrare un oggetto subito dopo averlo creato, è stato previsto un apposito parametro booleano nel costruttore che attiva automaticamente la registra- zione dopo l’inizializzazione dell’oggetto stesso. Sfruttando questo parametro il listato precedente si riduce ad una sola riga di codice:
var mouse = IFMouse ("mouse", true) ;
Nota
Una volta conclusa la fase di inizializzazione (OnInit), i comandi relativi all’aggiunta o alla rimozione di listener (compresa l’invocazione dei metodi Register() e Unregister() ) devono essere eseguiti sfruttando il meccanismo di esecuzione differita, come descritto nel paragrafo 2.2.6.
Si noti che i consumatori devono, in un qualche modo, ottenere il riferimento alla sor- gente di eventi. Questo è necessario sia per effettuare le operazioni di registrazione e de-registrazione, sia quando il consumatore ha bisogno di interrogare lo stato della sor- gente (in genere durante la gestione di un evento). Tradizionalmente questo problema viene risolto in due modi:
• Il riferimento alla sorgente viene passato come parametro al metodo di gestione dell’evento (direttamente o come campo del descrittore).
• Si memorizza il riferimento alla sorgente in una variabile di istanza del consumatore. La prima soluzione non consente però di ottenere il riferimento al di fuori del gestore dell’evento. Quanto alla seconda dobbiamo tener presente che, poiché il linguaggio S3D effettua una garbage collection basata su reference counting, la presenza di riferimenti ciclici (come quelli prodotti da questo approccio) comporta un memory leak. Questo è un problema da tener ben presente quando si scrive un programma S3D, ed ha influenzato non poche scelte nella progettazione del framework.
Ovviamente questo problema non si pone quando la sorgente in questione è l’oggetto “Ap- plicazione”. Infatti, come spiegato in precedenza, il suo riferimento è memorizzato nella variabile globale IFAPP, ed è quindi accessibile da qualsiasi punto del codice. Per quanto riguarda le altre sorgenti, la soluzione adottata consiste nell’inserire nello stato dell’appli- cazione una tabella hash in cui vengono memorizzati i riferimenti a tutte le fonti di eventi dello stato corrente. A tal scopo la classe IFApplication fornisce i metodi necessari per la gestione della tabella. L’inserimento in tabella di una fonte di eventi viene effettuato dal metodo Register() della fonte stessa, rendendo questa procedura completamente tra- sparente al programmatore. Con questo approccio i listener devono quindi memorizzare soltanto l’identificatore delle sorgenti a cui sono registrati, e poi reperire il riferimento corrispondente attraverso il metodo IFApplication::GetEventsSourceById().
Un’ultima considerazione: gli eventi vengono notificati ai listener seguendo lo stesso ordine con cui sono stati registrati. Per tale ragione i listener dovrebbero essere implementati facendo attenzione a non creare dipendenze logiche tra di loro. Esistono però casi in cui l’ordinamento dei listener è indispensabile, come nel caso dell’evento Rendering in presenza di elementi trasparenti o bidimensionali.