MokaByte 63 - Maggio 2002 

MokaShop il negozio online di MokaByte Progettare applicazioni J2EE multicanale
III parte: il pattern sorgente/ascoltatore e l'MVC

di

Giovanni
Puliti

Prosegue la serie di articoli dedicati alla progettazione di applicazioni J2EE. Il pattern MVC č molto potente e permette di creare applicazioniweb in modo molto flessibile e potente. Un'ultima piccola aggiunta consente di rendere il sistema ancora pių dinamico e praticamente in grado di soddisfare tutte le esigenze.

Introduzione
Questa serie di articoli è dedicata alla analisi delle tecniche e tecnologie utilizzabili per la realizzazione di applicazioni J2EE. Si è cominciato i mesi scorsi dalla progettazione di applicazioni web (servlet-JSP) tramite il modello MVC, e si proseguirà spiegando come integrare lo strato web con la business logic integrata di un layer EJB. Quanto mostrato fino ad oggi ha avuto come scopo principale quello di vedere come sia possibile separare lo strato di business logic da quello di presentation layer, creando infine una architettura flessibile e modellabile a piacimento.
Anche se dopo gli articoli dei mesi scorsi l'analisi del modello MVC in tutte le sue parti potrebbe considerarsi completata, prima di intraprendere l'analisi dello strato EJB, vorrei questo mese aggiungere una appendice a quanto detto in precedenza, mostrando come, tramite alcune tecniche di programmazione piuttosto semplici, ci si possa ulteriormente spingere nella direzione della customizzazione e personalizzazione dello strato web.
Quello che vedremo è come, tramite l'utilizzo del ben noto pattern sorgente/ascoltatore, sia possibile manipolare il flusso di lavoro di una web appllication in modo del tutto semplice ed intuitivo.
Importante notare che la teoria esposta fino ad oggi così come gli esempi mostrati, continueranno ad essere validi, ed anzi con poche modifiche al codice, le applicazioni che il lettore avrà realizzato, potranno essere integrate con i nuovi strumenti che andiamo presentando.

 

La struttura di base di una web application
Riprendendo brevemente la struttura di una web application basata sul modello MVC, si potrà ricordare come essa sia composta da tre componenti principali: la View, il Model, il Controller. La prima rappresenta lo strato software dedicato alla rappresentazione e visualizzazione dei dati, e secondo la terminologia UML, spesso corrisponde ad un use case. Il Controller invece è quel componente che gestise il controllo del traffico, ovvero in base alle invocazioni da parte del client, esegue determinate operazioni, o visualizza le viste corrispondenti. Infine il Model rappresenta la business logic della applicazione. In una web application semplice può essere costituito da una serie di Java Beans, mentre con il crescere delle esigenze, del carico di lavoro o della complessità funzionale, il tutto potrebbe essere supportato da uno strato EJB.
Senza entrare nei dettagli implementativi, per i quali si rimanda alle puntate predenti della serie, ed anche alle successive, vediamo per un momento di riconsiderare in modo schematico il funzionamento della web app MVC.
A partire da una pagina JSP, l'utente clicca su alcuni link, provocando l'esecuzione da parte di uno o più servlet di alcune classi, le quali passeranno infine il controllo nuovamente ad una pagina JSP oppure innoltreranno nuovamente il controllo ad altre classi di business logic.
Se si riconsidera per un momento il meccanismo di associazione View-Servlet-Action, si ricorderà come il tutto avvenga tramite alcuni semplici file di configurazione XML.

 


Figura 1 - schematizzazione del workflow in MVC

La Action rappresenta il cuore di tutta l'applicazione, ed il bravo programmatore sa che dovrà racchiudere al suo interno tutta la logica di esecuzione. Un buon programmatore sa anche che in genere è sempre bene mantenere più piccole possibile le classi di logica. L'esperto programmatore con gli anni ha però sperimentato che non sempre è possibile soddisfare queste due semplici regole. Così se il numero di righe di codice della Action cresce, si potrebbe decidere di sudividerla in sotto classi. Questa soluzione però implica che la Action contenga al suo interno chiamate alle altre classi, violando la regola che impone non codificare nel codice le macro regole di business logic, ad esempio utilizzando file XML per la definizione del workflow applicativo così come della associazione delle varie classi di logica.
Inoltre quasi sempe accade che successivamente alla realizzazione di una action, si desideri aggiungere nuove funzionalità non previste inizialmente, funzionalità che dovranno essere eseguite alla stessa invocazione da parte del client. Si immagini il caso in cui una ActionXX durante la sua esecuzione proceda ad effettuare alcune operazioni di modifica al database.
Se si considera come esempio la registrazione da parte di un utente ad una community web, questa fase potrebbe corrispondere all'inserimento dei dati utente nel database di community. Se l'analisi fatta in precedenza non è stata esaustiva, solo in un secondo momento ci si potrebbe rendere conto che a tale inserimento deve anche seguire un invio di una mail all'utente, una tipica operazione asincrona. Come si vedrà fra poco l'asincronicità di questo tipo di operazione è uno dei requisti più importanti tale da giustifica la tecnica che stiamo per presentare.
Per effettuare tale modifica dopo la ActionXXX si potrebbe mettere in cascata una seconda action: tale soluzione ha la sua giustificazione, anche se può complicare non poco la leggibilità del workflow della applicazione. Si pensi ad esempio a cosa accadrebbe se si volessero aggiungere più action in cascata nel caso in cui una di queste producesse un errore. In ogni caso si deve mettere mano al codice, e questo è probabilmente la cosa che più di tutte si vorrebbe evitare.
Esiste una soluzione più elegante, che si ispira al pattern sorgente/ascoltatore, utilizzato anche nel modello ad eventi di Java.

 

Il pattern sorgente-ascoltatore
Questo schema progettuale è diventato famoso nel mondo Java quando fu introdotto il delegation model nel JDK 1.1 per fornire uno strumento più potente per la gestione degli eventi di una interfaccia grafica. Noto anche sotto il nome di Delegation Model, ha assunto nel tempo un ruolo molto importante: l'obiettivo principale è quello di consentire la propagazione del workflow da una punto centrale verso n punti periferici. Nelle Gui ad esempio un evento di MouseClick potrebbe essere propagato a tutti quei componenti interessati a gestire tale evento.
Il meccanismo principale si basa sulla presenza di una sorgente di eventi e di n ascoltatori passivi, i quali pero' dovranno attivamente registrarsi presso la sorgente. Quest'ultima mantiene quindi una lista di ascoltatori i quali a loro volta dovranno rispettare una interfaccia ben precisa (ad esempio offrire un metodo pubblico di invocazione da parte di esterni).
Ogni volta che la sorgente genera un evento in modo più o meno automatico, verrà scorsa la lista degli ascoltatori invocando uno ad uno il metodo di invocazione.
Anche se in principio tale schema può apparire complicato, il paragrafo successivo mostra con del codice, come in realtà il tutto sia molto semplice. Per chi fosse interessato ad avere maggiori approfondimenti in merito può consultare le fonti [delegation] in bibliografia.

 

ListenerAction e FireAction
Il meccanismo di esecuzione della logica contenuta in una action è molto semplice: il ServletRouter, dopo avere ricavato il nome della classe da utilizzare, ne invoca il metodo perform(). Utilizzando una terminologia differente, si può dire che il servlet invia un messaggio alla action. In questo contesto, si potrà inserire lo schema sorgente ascoltatore in modo tale che il metodo action, oltre a svolgere il suo normale funzionamento, possa effettuare un broadcast dell'evento verso altre action. Con poche modifiche al codice si può quindi modificare una action in modo che possa sia gestire la lista degli ascoltori, la registrazione di nuovi listener, o la rimozioni di quelli già registrati, oltre a fornire ovviamente il meccanismo di broadcast. Di seguito sono riportate le parti di codice più importanti di questa nuova classe, che si potrebbe chiamare FireActionImpl, in conseguenza del fatto che implementa l'interfaccia FiredAction

public class FireActionImpl extends ActionImpl
                            implements 
FireAction {

  private Vector Listeners;

  public FireActionImpl() {
    Listeners = new Vector();
  }

  public synchronized void addListener(Action a)
                           throws Exception {
    Listeners.addElement(a);
  }

  public synchronized void removeListener(Action a)
                           throws Exception {
    if (Listeners.indexOf(a) !=-1)
      Listeners.removeElement(a);
    else
      throw new Exception("Exception in
                           FireActionImpl.removeListener():
                           element "+a+
                          
" not found in list of listeners");
  }

  public void fireEvent(HttpServlet servlet,
                        HttpServletRequest req,
                        HttpServletResponse res)throws Exception{
    Action action;
    for (int i=0; i<Listeners.size(); i++){
      action=(Action)Listeners.elementAt(i);
      action.perform(servlet, req, res);
    }
  }

  public String perform(HttpServlet servlet,
                        HttpServletRequest req,
                        HttpServletResponse res) throws Exception {
    this.fireEvent(servlet, req, res);
    return null;
  }

  public static void main(String[] args) {
    FireActionImpl fireActionImpl1 = new FireActionImpl();
  }

}

Analizzando il codice si può comprenderne facimente il funzionamento: una fire-action quando riceve un messaggio dal servlet (invocazione del metodo perform), per prima cosa oltre ad eseguire le usuali operazioni di questo metodo metodo, invoca il fireEvent(). Tale metodo cicla su tutti gli ascoltatori, che a loro volta saranno classi Action, invocandone il metodo perform(), dando vita così ad un vero e proprio broadcast di messaggi.
La classe FireAction mantiene quindi una lista (un Vector) di actions che dovranno essere invocate per il broadcast del messaggio, e pubblica due metodi, addListener() e removeListener(), per permettere agli ascoltatori di registrarsi e/o rimuoversi dalla lista degli ascoltatori. Saranno quindi i vari ascoltatori che, tramite la registrazione, comunicheranno alla FireAction di essere interessati a ricevere notifica del messaggio, ogni volta che il RouterSerlvet invoca il metodo perform della FireAction, ovvero ogni volta che la FireAction riceve un messaggio dal servlet.


Figura 2
- fork-threadmultipli

La presenza di una interfaccia FireAction impone il design by contract di tutte le classi che vorranno seguire questo schema (ovvero impone la presenza dei metodi di registrazione e deregistrazione dei vari listener).
Per realizzare il legame fra un action sorgente e le n action ascoltatrici si dovranno introdurre alcune modifiche sia nel ServletRouter sia nel file XML actions.xml. Ad esempio per associare alla ActionXXX una serie di actions ascoltatrici ActionListener1, ActionListener2, ActionListener3, si potrebbe modificare il file XML da

<action NAME="actionx" CLASS="com.mokabyte.ActionXXX">
<executed>code-ok</executed>
<failed>code-ko</failed>
</action>

a questa versione, che associa tramite un nuovo tag, alcune action come listener della ActionXXX

<action NAME="actionx" CLASS="com.mokabyte.ActionXXX">
<executed>code-ok</executed>
<failed>code-ko</failed>
<listener>ActionListener1</listener>
<listener>ActionListener2</listener>
<listener>ActionListener3</listener>
</action>

Mentre il servlet dovrà effettuare il caricamento di una sorgente e dei suoi listener.
E' importante notare che, mentre per introdurre il concetto di fire action si è dovuto introdurre una classe apposita, ogni Action può essere ascoltatrice di una FireAction senza dover effettuare nessuna modifica.

 

Biforcazioni del workflow e thread di esecuzione multipli
Benché l'introduzione delle fire action è molto utile per risolvere molte situazioni permettendo di aggiungere molte funzionalità alla web application in modo semplice ed intuitivo, ha comunque una forte limitazione. Infatti introducendo uno schema sorgente-ascoltatore in un determinato punto della applicazione, si da vita ad una biforcazione del workflow della applicazione, senza poter in alcun modo mantenere un qualche tipo di controllo sulla sequenzialità delle operazioni. Detto in modo più semplice, tutte le volte che una fire action esegue il metodo perform, passando da un punto A ad un punto B dello schema della applicazione, tutte le operazioni effettuate dai listener verranno eseguite in modo parallelo ed asincrono. Se nella maggior parte dei casi questo può essere utile, ed anzi frutto di una precisa esigenza applicativa, può complicare le cose quando si verificano errori in uno dei listener o peggio ancora nella sorgente. In questo caso infatti il thread di esecuzione non sarà uno solo e la gestione dell'errore dovrà essere effettuata in modo più complesso.


Figura 3
- il pattern sorgente-ascoltatore introduce
il problema dei thread asincroni

Proprio a causa della presenza di più thread di esecuzione, le operazioni svolte dai vari listener non possono essere assimilate nello stesso contesto transazionale di quello principale (quello della fire action).
Per questo motivo i vari listener dovranno essere utilizzati ed inseriti nella logica della applicazione con molta attenzione, e comunque sempre con lo scopo di eseguire operazioni secondarie in logica asincrona, come l'invio di una mail, la scrittura di un file di log, e così via.
Quello del log è un caso particolarmente adatto a questo tipo di scenario; se in una web application, tutte le action sono sostituite da fire action, si potranno introdurre in ogni istante e su ogni punto della applicazioni funzionalità di log in modo da tracciare l'insorgere di particolari problemi. Si pensi ad esempio al caso in cui, per ogni passaggio da un punto A ad un punto B, si proceda alla memorizzazione di una serie di informazioni circa lo stato, il client e la particolare posizione dei punti A e B all'interno della struttura della web application. Tutte queste informazioni potrebbero essere utilizzate per tracciare il percorso seguito dall'utente all'interno dell'ipertesto rappresentato dalla applicazione, cosa particolarmente utile sia per effettuare statistiche o per individuare particolari errori.

 

Conclusione
La tecnica qui presentata rappresenta una interessante variazione del classico modello MVC, anche se a tutti gli effetti non introduce nessuna alterazione al pattern in esame, per cui potrà essere utilizzata in modo trasparente senza impattare sulla architettura della applicazione. Sebbene presente alcune limitazioni, è questa sicuramente la sua caratteristica più interessante e potente.

 

Bibliografia
[mokashop1] - "MokaShop: come progettare una applicazione multicanale - I parte" di Giovanni Puliti, MokaByte 60, Febbraio 2002
www.mokabyte.it/2002/02

[mokashop2] - "MokaShop: come progettare una applicazione multicanale - II parte" di Giovanni Puliti, MokaByte 61, Marzo 2002
www.mokabyte.it/2002/02

[observ-t] - "Il pattern Observer - la teoria" di Lorenzo Bettini - MokaByte 26, Gennaio 1999
www.mokabyte.it/1999/01

[observ-p] - "Il pattern Observer - la pratica" di Andrea Trentini- MokaByte 26, Gennaio 1999
www.mokabyte.it/1999/01

[delegation] - "Corso di Swing IV parte - Il Delegation Model" di Massimo Carli - MokaByte 30 Maggio 1999
www.mokabyte.it/1999/05

 
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it