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
|