|
In questo capitolo si mette in pratica quanto detto sinora al fine di costruire una implementazione pratica del pattern Registry per la messa in uso di una sessione thread-scoped. Per spiegare che cosa intendo per sessione, basti pensare alla javax.servlet.HttpSession; consiste in una sorta di cache dove poter memorizzare dei valori associati a dei nomi e successivamente, attraverso tali nomi, recuperare i valori. Tale sessione viene utilizzata da componenti (JSP, Servlet) che non sono a conoscenza l'uno dell'altro, ma eventualmente comunicano inserendo e recuperando degli oggetti (nella sessione). In ambito di sviluppo di applicazioni distribuite, il concetto di thread è stato lasciato (ingiustamente) in disparte, forse alle volte per una erronea interpretazione delle specifiche J2EE che vietano l'utilizzo dei Thread all'interno degli EJB. Ebbene, bisogna ricordare che, seppur osservando tali specifiche alla lettera, il concetto di thread esiste a prescindere dal fatto che noi vogliamo utilizzarli o meno. In java tutto ciò che è dinamico è un thread; un metodo viene eseguito dai thread; il flusso di sequenza di una applicazione viene eseguito dai thread, ciò che in un sequence diagram permette di spostarsi da un partecipante ad un altro (le frecce) chiamando metodi, e spedendo messaggi è un thread. Analizziamo il sequence diagram presente in figura 1. Al di là del flusso che descrive, notiamo l'esistenza di una serie di partecipanti coordinati e creati da una sequenza di operazioni al fine di realizzare un determinato scopo. Anche se non vengono specificati i ritorni (le frecce da destra verso sinistra) con una penna si può seguire benissimo il flusso che parte dall'attore principale (l'omino in alto a sinistra), crea il primo oggetto e via via sino ad arrivare all'ultimo partecipante che ritornando un certo risultato all'utilizzatore, chiude il giro.  Fig. 1 Implementazione di una sessione thread-scoped : il pattern Registry. Partendo dal presupposto che -
non vengono creati esplicitamente nuovi thread da codice all'interno del flusso; -
i componenti che intervengono nel diagramma di sequenza si trovano tutti all'interno della stessa JVM; cioè non esistono chiamate remote tra i partecipanti. ci domandiamo: chi è che esegue tutti gli step descritti nel diagramma con la giusta sequenza, interrogando i componenti nell'ordine descritto? Si tratta di un thread, il quale, fermo restando i presupposti di cui sopra, passa attraverso tutti gli oggetti invocandone i metodi e completando il flusso descritto. All'interno dei metodi possiamo ottenere una reference a questo thread tramite il metodo Thread.currentThread(). L'idea di fondo di questo esempio si basa proprio sul concetto appena visto: l'esistenza di un comune oggetto (il thread esecutore) tra tutti gli step di un determinato processo. Supponiamo che l'ultimo partecipante del diagramma (quello più a destra) abbia bisogno di una particolare oggetto che viene creato invece in un metodo faiQualcosa() del primo partecipante (quello più a sinistra dopo l'omino). La prima soluzione che viene in mente è quello di passare come parametro tale oggetto a chi se ne deve servire. Esiste in tal senso un piccolo problema: il produttore dell'oggetto ed il suo consumatore non sono legati direttamente, ma tramite una miriade di altri oggetti che intervengono nel flusso su step intermedi. Secondo la soluzione proposta dovremo passare come parametro l'oggetto in questione tra tutti i componenti intermedi tra i due partecipanti, obbligando quindi a sporcare le interfacce (che presentano nella firma dei metodi il parametro in questione che in realtà non serve a nulla, solamente come passamano per poter arrivare all'utilizzatore finale). La soluzione più pulita risulta essere una sorta di cache in cui memorizzare e successivamente recuperare l'oggetto che è necessario condividere. La prima cosa che viene in mente in tal senso è un singleton. Ma se lo stato di tale oggetto cambia ogni qualvolta che il flusso descritto nel sequence ha inizio, allora forse una persistenza (in memoria) come quella offerta dal singleton è superflua ed obbliga ad implementare meccanismi di refresh (i singleton si prestano molto bene per mantenere delle cache di dati immutabili di sola lettura). Quello che serve è una cache "temporanea" che ha un ciclo di vita legato alla durata del thread esecutore (thread-scoped). Qui i produttori memorizzano gli oggetti da riutilizzare associandogli un nome e i consumatori li recuperano (tramite quel nome) in seguito utilizzandoli. Tale soluzione è completamente indipendente dal modo in cui sono relazionati gli oggetti in questione: basta semplicemente che facciano parte del flusso di sequenza (si trovino sulla strada del thread esecutore...). 5.1 I componenti. Le classi coinvolte nella implementazione di una sessione ad uso thread sono molto semplici e sono due. Vediamo brevemente come sono fatte e come si relazionano tra di loro nel class diagram in figura 2.  Fig. 2 Implementazione di una sessione thread-scoped : il pattern Registry. Come si capisce dal nome, la classe ThreadSession è l'implementazione della sessione (il Registry dove si prelevano e recuperano gli oggetti memorizzati). Contiene quindi i metodi per memorizzate (setAttribute), recuperare (getAttribute) gli oggetti, pulire la cache (clear), etc... Gestisce una Map interna in cui sono presenti gli attributi memorizzati con i loro valori. La classe ThreadSessionManager funge da coordinatore e gestore delle sessioni attive, fornendo un unico punto di ingresso (trasparente per l'utilizzatore) per ottenere/creare la sessione specifica dell'utilizzatore. Si tratta in sostanza di un singleton che mantiene una variabile privata di tipo java.lang.ThreadLocal in cui memorizza gli oggetti di tipo ThreadSession creati durante l'esecuzione dell'applicazione. Quando viene richiesta una sessione, si analizza, tramite il metodo Thread.currentThread(), il richiedente, viene verificato (tramite la classe ThreadLocal) se esiste una sessione associata ad esso e in caso positivo viene ritornata, mentre in caso negativo, viene creata, memorizzata e ritornata. Una successiva richiesta da parte dello stesso thread provocherà il recupero della sessione memorizzata (e creata) precedentemente. 5.2 L'esempio. Ai fini di una migliore comprensione del funzionamento nonché dell'utilizzo dei concetti presentati sinora, prendiamo in considerazione un ipotetico scenario in cui esistono tre classi Prima, Seconda e Terza legate a catena tra di loro; cioè la classe Prima crea la classe Seconda la quale crea la classe Terza. Fig. 3 Implementazione di una sessione thread-scoped : il pattern Registry public class Prima { public int esegui(String codice) { // X può essere ottenuto solamente in questo punto X x = businessDelegate.recuperaOggettoX(codice); Seconda seconda = new Seconda(); double risultato = seconda.eseguiCalcolo(x.getQuantita); return arrotonda(risultato); } public int arrotonda(double valore) { .... } } public class Seconda { public double eseguiCalcolo(int quantita){ int moltiplicatore = ... vengono eseguiti una serie di calcoli Terza terza = new Terza(); return terza.eseguiCalcoloPercentuale(moltiplicatore); } } public class Terza { public double eseguiCalcoloPercentuale(int moltiplicatore) { // per eseguire questo calcolo abbiamo bisogno dell'oggetto X // Creato nella classe Prima return .... } } Sia la classe Prima che la classe Terza (la prima e l'ultima della catena) hanno bisogno, per compiere il proprio lavoro, di un oggetto X che contiene un informazione quantitativa da utilizzarre. Le informazioni necessarie a recuperare tale oggetto sono a disposizione solamente della classe Prima attraverso l'istanza del business delegate ed il parametro passato in ingresso al suo metodo esegui(String codice). Il problema è per l'appunto come far arrivare tale oggetto X all'istanza di Terza di modo che anch'essa possa compiere il suo lavoro. Un primo approccio è quello di fare sì che la reference di X sia presente nella firma di ogni metodo intermedio e finale, cambiando così le classi in questo modo: nella classe Seconda : public double eseguiCalcolo(int quantita, X oggetto){ int moltiplicatore = ... vengono eseguiti una serie di calcoli Terza terza = new Terza(); return terza.eseguiCalcoloPercentuale(moltiplicatore,oggetto); } mentre nella classe Terza: public double eseguiCalcoloPercentuale(int moltiplicatore, X oggetto) { // per eseguire questo calcolo abbiamo bisogno dell'oggetto X // Creato nella classe Prima return oggetto.getQuantita * UNA_COSTANTE / 100.87; } In questo modo Terza ha a disposizione l'oggetto creato nella classe Prima e tutto funziona. Certo che, da un punto di vista di pulizia del codice il tutto lascia un pò a desiderare, visto che siamo obbligati a far passare tale reference tra tutti gli step intermedi (nell'esempio ce ne è uno solo, ma provate ad immaginare la situazione come si presenterebbe nel sequence diagram riportato in figura 1...!) ed inoltre a sporcare le interfacce con parametri che, per quanto riguarda gli step intermedi, hanno solamente una funzione di transito: ad un primo sguardo non è molto chiaro a cosa serva il parametro oggetto nel metodo della classe Seconda. La soluzione più pulita prevede l'utilizzo di una sessione ad uso del thread esecutore del flusso che abbiamo analizzato. Partiamo da come cambia il codice delle tre classi. public class Prima { public int esegui(String codice) { X x = businessDelegate.recuperaOggettoX(codice); // Ottenimento della sessione per questo thread corrente // e memorizzazione della reference x con chiave stringa "OGGETTO_X". Tale // stringa verrà poi utilizzata (classe Terza) per recuperare x. ThreadSessionManager.getInstance().getSession().setAttribute("OGGETTO_X",x); // Oppure in forma meno concisa... ThreadSession session = ThreadSessionManager().getInstance().getSession(); session.setAttribute("OGGETTO_X",x); Seconda seconda = new Seconda(); double risultato = seconda.eseguiCalcolo(x.getQuantita); return arrotonda(risultato); } public int arrotonda(double valore) { .... } } // La classe Seconda non presenta più alcuna traccia di X public class Seconda { public double eseguiCalcolo(int quantita){ int moltiplicatore = ... vengono eseguiti una serie di calcoli Terza terza = new Terza(); return terza.eseguiCalcoloPercentuale(moltiplicatore); } } public class Terza { public double eseguiCalcoloPercentuale(int moltiplicatore) { // Recupero della sessione associata a questo thread e dell'attributo "OGGETTO_X" // precedentemente creato e memorizzato nella sessione. X x = (X) ThreadSessionManager.getInstance().getSession().getAttribute("OGGETTO_X"); // Oppure in forma meno concisa... ThreadSession session = ThreadSessionManager.getInstance().getSession(); X x = (X)session.getAttribute("OGGETTO_X"); return x.getQuantita * UNA_COSTANTE / 100.87; } } Viene di seguito riportato il sequence diagram che mostra l'utilizzo della ThreadSession. Fig. 4 Implementazione di una sessione thread-scoped : il pattern Registry. Notare come la classe Prima nello step 2 e 3, interfacciandosi con il ThreadSessionManager ottiene la sessione associata (associata al thread corrente). Tale operazione in realtà provoca (step 4) la creazione di una nuova istanza di un oggetto di tipo ThreadSession (è la prima volta che il thread richiede una sessione, non esiste e quindi viene creata). Successivamente (step 5,6) il componente Prima crea l'oggetto X e lo inserisce nella sessione (metodo setAttribute). Continua poi il flusso di esecuzione sino ad arrivare alla classe terza, dove per effettuare il calcolo si necessita ancora di questo oggetto X. A questo punto il gioco è semplice: attraverso (step 11 e 12) il ThreadSessionManager si richiede la sessione associata al thread corrente (che è sempre lo stesso di prima!). Tale richiesta non provoca più la creazione di un nuovo oggetto, bensì la localizzazione della sessione associata al thread richiedente (step 13). Una volta ottenuta la sessione, l'oggetto di tipo Terza può richiedere l'attributo X precedentemente memorizzato (step 14 - metodo getAttribute) ed effettuare così i propri calcoli. Ripeto,l'esempio presentato è alquanto semplice ed astratto, ma basti pensare ad un sequence come quello presentato all'inizio per immaginare come tale problema assuma delle dimensioni rilevanti. Le applicazioni pratiche di quanto presentato sinora sono a mio avviso numerose, poiché quando si parla di riutilizzo di oggetti, ottimizzazione, razionalizzazione delle risorse, non si finisce mai di effettuare aggiustamenti al proprio codice. Tenendo conto anche del fatto che la tecnica presentata costituisce un modo elegante per mettere in comunicazione due oggetti non direttamente legati tra di loro. Molti design pattern presentano una situazione di questo tipo; vediamo alcuni esempi. Il pattern Chain of Responsibility costituisce una catena di oggetti in cui ognuno conosce l'anello successivo verso cui delegare eventualmente. Notare come in Fig. 5, aClient non conosce il secondo aConcreteHandler, né il successor contenuto in esso.  Fig. 5 Implementazione di una sessione thread-scoped : il pattern Registry Il design pattern Decorator definisce una struttura annidata in cui si presenta una situazione molto simile a questa vista appena per il Chain of Responsibility. Ogni Decorato è annidato dentro il suo Decoratore in una struttura con una profondità indefinita a priori.  Fig. 6 Implementazione di una sessione thread-scoped : il pattern Registry Il pattern Composite definisce una struttura ad albero di oggetti in cui ogni componente può essere nodo o foglia. Nella figura aComposite non è a conoscenza delle aLeaf appartenenti al aComposite figlio.  Fig. 7 Implementazione di una sessione thread-scoped : il pattern Registry
|