La
propagazione dei dati fra strati differenti: il pattern
DTO
Uno
dei problemi più frequenti da risolvere quando
si deve mettere in comunicazione strati ap-plicativi
differenti é quello di trasferire le informazioni
fra i vari moduli che compongono l'ap-plicazione nel
complesso. Riconsiderando l'esempio che ci ha accompagnati
per tutti questi mesi, si può a tal proposito
analizzare il meccanismo di modifica del proprio profilo
da parte di un utente registrato. Le operazione da effettuare
sono in questo caso due:
- l'utente
inserisce i propri dati userid e password
- i
dati vengono passati ad una action apposita (ad esempio
la LoadProfileAction) la quale si preoccupa di verificare
se tali dati sono corretti (login) e di caricare i
dati u-tente (load profile) in modo da visualizzarli
a video
Le due operazioni di login e load profile saranno
portate a termine grazie all'invocazione di due appositi
metodi di un session bean remoto (CommunityManager).
Come
dovrebbe essere ormai noto l'invocazione dei metodi
remoti di un EJB comporta il tran-sito di informazioni
sullo strato di rete, per cui sarebbe in ogni caso raccomandabile
ridurre al minimo le chiamate remote verso il session
bean. Si vedrà in seguito come risolvere questo
problema: per il momento si supponga che il metodo loadProfile()
del session bean CommunityManager non effettui nessun
controllo di validità sui dati ma si limiti a
restituire le informa-zioni relative ad un determinato
userid.
Tale metodo dovrà quindi interfacciarsi con altri
enterprise beans in modo da andare a cercare nel database
i dati e restituirli al chiamante. Nel caso in esame
in particolare la ricerca ed il ca-ricamento dei dati
viene effettuato tramite un entity beans.
Dato che non si vuole legare lo strato EJB con quello
invocante é necessario trovare un sistema per
estrapolare i dati dall'entity, passarli al session
e poi al chiamante: per questo si deve evita-re nel
modo più assoluto che un oggetto dello strato
server (un entity o un qualche Value O-bject) sia passato
al client.
La soluzione a questo semplice problema é quella
di trasferire le informazioni dall'entity ad un oggetto
serializzabile (in modo da trasferirlo in rete come
parametro di risposta del metodo re-moto del session),
ed inviare tale oggetto dal server verso il client.
Questo schema porta alla realizzazione di un oggetto
detto Data Transfert Object (pattern DTO). Se l'oggetto
di partenza è un generico User, allora si può
pensare di creare un DTO relativo detto UserDTO. Per
creare tale struttura dati di trasferimento si possono
seguire due strade: utilizzare un DTO basato su strutture
dati generiche (pattern GenericDTO) o uno appositamente
costruito (CustomDTO).
Nel primo caso tutte le informazioni dell'utente verranno
estrapolate dall'entity ed inserite in una collezione
di qualche tipo (tipicamente un oggetto Pro-perties)
presente fra le librerie standard del JDK: sia il client
che il server dispongono del byte-code del generic DTO
per cui le operazioni di serializzazione e deserializzazione
così come il processo di trasferimento in rete
potrà essere effettuato senza problemi. Lo svantaggio
di que-sta soluzione é che, nel caso di un oggetto
properties, non offre nessun supporto sul controllo
dei tipi e dei nomi dei campi: ad esempio il ricevente
del DTO non può avere la certezza della presenza
di un campo userFirstName al posto di UserFirstName
o semplicemente firstName. L'operazione di accesso ai
dati del properties avviene tramite una get(<nome-chiave>)
che nel caso in cui la proprietà non sia trovata
restituisce un null.
Il
GenericDTO quindi rappresenta una soluzione semplice
ma non garantisce la correttezza della applicazione
se non in fase di esecuzione al verificarsi di eccezioni
di tipo NullPointerException.
Inoltre questo tipo di DTO può essere scomodo
da gestire se la struttura dati da trasferire risulta
essere particolarmente complessa: si pensi al caso in
cui l'oggetto User contenga al suo interno riferimenti
ad altri oggetti (dependents objects).
Un CustomDTO invece è una struttura dati appositamente
pensata per trasferire le informazio-ni dell'utente
fra i vari strati: il nome dei campi i tipi e la struttura
dati complessiva risulta ben definita sia per il client
che per il server e non si potranno avere errori in
esecuzione.
L'errato accesso ad un campo non esistente causa un
errore in compilazione, che potrà essere facilmente
corretto.
Il prezzo da pagare in questo caso é dato dalla
necessità di distribuire il bytecode della classe
UserDTO su tutti gli strati che la utilizzano. Questo
significa che per ogni modifica alla strut-tura dati
del DTO sarà necessario ricompilare e ridistribuire
i file .class corrispondenti.
Spesso nei casi concreti questo processo, per quanto
concettualmente piuttosto semplice, porta alla generazione
di problemi di difficile soluzione (non perché
complessi in senso assoluto, ma perché non immediati
da riconoscere).
In definitiva non esiste una regola universalmente valida
per la scelta fra un Generic DTO ed un Custom DTO. Molto
dipende dal caso in esame e dalla complessità
dei dati. Nel caso in cui si voglia ridurre al minimo
i problemi di ClassCastException o Invalid SerializationUID
una buona documentazione sulla struttura dati da trasferire
consente di utilizzare un Generic DTO, con indubbi vantaggi
nelle fasi di sviluppo e deploy delle varie parti e
strati applicativi.
Integrare
lo strato di presentazione con quello di business logic:
il pattern Business Delegate
Tutte le volte che un client deve effettuare una invocazione
remota di tipo EJB deve eseguire una serie di operazioni
sequenziali volte nell'ordine ad inizializzare il contesto
EJB, ottenere il reference remoto della Home Interface
dell'oggetto, ricavare l'interfaccia remota ed invocare
infine il metodo remoto.
I primi tre passi, oltre ad essere sempre uguali a se
stessi, richiedono l'utilizzo di parametri e valori
(i parametri di inizializzazione del contesto o i nomi
dei reference EJB remoti) che de-vono essere passati
al client.
L'utilizzo di una classe client-side appositamente pensata
per lavorare in accoppiamento con il Session Façade
EJB (vedi oltre), permette di svincolare l'applicazione
client dal tenere a mente tali parametri e di rendersi
più indipendente da ciò che accade dietro
sul lato server.
Riconsiderando l'esempio della Community di MokaByte,
lo strato web conterrà una classe LoginAction
deputata alla realizzazione del login tramite il supporto
di un session remoto. Tale classe invocherà il
metodo login() di LoginManagerSF, un Session Façade
che nasconde con un solo metodo tutta la logica relativa
al login remoto.
Per svincolare la action dello strato web dallo strato
EJB, si può pensare di accoppiare al Ses-sion
Façade un oggetto isomorfo (ovvero con stessi
metodi e stessi parametri): questo pattern, detto Business
Delegate ([BD]) porta alla generazione di un oggetto
client-side che dall'esterno appare come una banale
classe Java, ma al suo interno contiene tutta la logica
di accesso remoto EJB.
Tale classe, LoginManagerBD, riceverà una volta
per tutte nel costruttore i parametri di inizia-lizzazione
del contesto JNDI ed effettua al suo interno la lookup
della home interface del Ses-sion Façade.
Dalla home interface il metodo login() del Business
Delegate ricava la remote ed invoca il me-todo remoto
del session.
Con un semplice esempio si può avere chiaro come
tutto ciò possa essere implementato:
public
class CommunityManagerBD {
private CommunityManagerSFHome managerHome = null;
Properties JndiProperties;
public CommunityManagerBD(Properties JndiProperties)
throws Exception{
this.JndiProperties = JndiProperties;
initialize();
}
// da utilizzare nel caso in cui
// si inizializza il contesto direttamente
// da dentro il container EJB
public CommunityManagerBD() throws Exception{
this.JndiProperties = new Properties();
initialize();
}
public Properties login(String id, String
pswd) throws Exception{
CommunityManagerSFRemote manager
= null;
try {
manager = managerHome.create();
System.out.println("manager"
+ manager);
Properties ret =
manager.login(id, pswd);
return ret;
}
catch (RemoteException ex) {
throw new Exception("Exception
CommunityManagerBD.login():\n"
+
ex.getMessage());
}
catch (CreateException ex) {
throw new Exception("Exception CommunityManagerBD.login():\n"+
ex.getMessage());
}
}
public
void initialize() throws Exception {
try {
//get naming context
Context context = new
InitialContext(JndiProperties);
//look
up jndi name
Object
ref=context.lookup("CommunityManagerSF");
//look
up jndi name and cast to Home interface
managerHome
=(CommunityManagerSFHome)
PortableRemoteObject.narrow(ref,
CommunityManagerSFHome.class);
}
catch (Exception e) {
throw new Exception("Exception
in initialize():\n"+
e.getMessage());
}
}
public static void main(String[] args) throws
Exception{
Properties props = new Properties();
props.put("java.naming.factory.initial",
"org.jnp.interfaces.NamingContextFactory");
props.put("java.naming.factory.url.pkgs",
"org.jboss.naming:org.jnp.interfaces");
props.put("java.naming.provider.url",
"jnp://<servername>:1099");
CommunityManagerBD
cmbd = new CommunityManagerBD(props);
Properties ret = cmbd.login("paperino","pippo");
System.out.println("Login
effettuato "+ret);
}
}
Si
noti come all'interno del metodo main, dopo aver creato
un Properties con le proprietà per l'accesso
al contesto JNDI del container EJB (in questo caso JBoss
3.xx), non vi sia nessun ri-ferimento a logica e sintassi
EJB: il business delegate ha nascosto tutte le operazioni
complesse e ripetitive.
Centralizzare le chiamate: il pattern Session Façade
Lo strato applicativo client ogni volta che effettua
una chiamata verso il layer EJB opera una invocazione
RMI remota che avviene tramite lo strato di rete TCP.
Ovviamente questo modo di operare, sebbene introduca
un notevole livello di indirezione e di disaccoppiamento
fra i due strati, comporta un maggior sovraccarico e
quindi un rallentamento complessivo. Per questo motivo
le chiamate remote dovrebbero essere limitate al minimo
possibile in modo da non im-pattare troppo sulle prestazioni.
Purtroppo, nel caso in cui si sia fatta la scelta di
isolare tutta la business logic della applicazione sullo
strato EJB, risulta difficile attuare una qualche forma
di economia sulle chiamate. Ogni controllo sui dati
così come ogni operazioni non elementare de-ve
per forza di cose corrispondere ad una chiamata remota.
Una buona architettura EJB prevede l'utilizzo di vari
session beans per inglobare le funzionali-tà
da mettere a disposizione del client: si pensi ad esempio
alla applicazione disponibile sul sito MokaByte per
la gestione dei download dei libri in formato elettronico.
In questo caso un ses-sion LoginManger (in esecuzione
nell'application server dopo il deploy della applicazione
Community) potrebbe occuparsi di tutte le operazioni
relative alla gestione degli utenti e della profilazione,
mentre un altro bean potrebbe gestire tutte le operazioni
di download dei file e di gestione dei log. Una applicazione
leggermente più complessa potrebbe essere composta
da vari session ognuno dei quali incaricato di una operazione
ben precisa.
Nel caso in cui il client debba avvalersi di ogni singolo
session bean in modo atomico, si a-vrebbero molte chiamate
di rete con dannosi effetti sulle prestazioni. Non è
detto però che ogni singolo session debba interagire
con il client direttamente, o meglio che il client debba
dirigere in modo sequenziale tutte le operazioni: se
ad esempio per poter scaricare un libro si dovesse effettuare
il login, le operazioni login+download potrebbero essere
accorpate ed eseguite in modo automatico senza l'intervento
del client. Il patter Session Façade ([SFP])
svolge proprio questo compito: raggruppare le chiamate
a metodi elementari in metodi di un super-session be-an
che svolge il compito di interfaccia verso lo strato
client.
Nel caso in cui i vari session bean nascosti dietro
il Session Façade espongano una interfaccia locale
([LOCAL]), si ridurrà ulteriormente il traffico
di rete, riducendolo alle sole poche chia-mate client-session
SF.
Figura 1 - Il session façade permette
di isolare le chiamate remote
EJB a pochi soli metodi invocati direttamente dal client.
Nel
caso in cui non sia possibile raggruppare le chiamate
dei session di servizio in metodi co-muni del Session
Façade, è buona cosa comunque introdurre
questo strato applicativo al fine di creare un proxy
che separi ulteriormente il cliente da ciò che
sta sul server: nel caso in cui si dovessero modificare
i session di servizio (sia nei nomi che nei metodi),
lo strato client non ne sarà coinvolto. L'utilizzo
di un Session Façade ridotto al minimo, che di
fatto realizza il pattern proxy, potrebbe far pensare
ad un inutile impatto sulle prestazioni: e questa una
considerazione non priva di senso, e per una risposta
vale la pena valutare attentamente costi e benefici
di una tale organizzazione (magari con un po' di test
pratici).
Disaccoppiare lo strato di persistenza: il pattern DAO
Dato che ogni applicazione che si rispetti deve prima
o poi accedere al database per leggere o scrivere i
dati corrispondenti alle strutture dati utilizzate (si
pensi agli entity beans che mappa-no dati di una o più
tabelle relazionali), è buona norma anche in
questo caso realizzare qualche schema progettuale in
modo da separare l'oggetto-dato dallo strato di persistenza.
Il pattern DAO ([DAO]) in questo caso permette di risolvere
brillantemente questo problema, isolando tutta la logica
di lettura e scrittura della struttura dati.
Si riconsideri il caso della struttura dati della classe
User ed alla relativa UserDTO per trasferi-re i dati
da e verso lo strato client. La UserDTO potrebbe essere
passato come parametro in let-tura e scrittura verso
un UserDAO per il caricamento e salvataggio dei dati
sul database.
Lo schema potrebbe essere quello riportato in figura
2:
Figura 2 - Una ipotetica gerarchia di classi per
il pattern DAO
Di
seguito è riportato un semplice esempio di DAO
per la classe User: come si può notare in questo
caso le operazioni di set/get sono effettuate con una
semplice serializzazione su file system. Nel caso in
cui si volesse realizzare qualcosa di più realistico
utilizzando un database relazionale per le operazioni
di lettura e scrittura, è sufficiente ridefinire
i corpi dei metodi setUserData() getUserData() inserendo
la necessaria logica SQL-JDBC.
public
class UserDAO {
String StoragePath = "c:\\temp\\users-data\\";
public User getUser(String userId) throws
Exception {
FileInputStream fis = new FileInputStream(StoragePath+userId+".ser");
ObjectInputStream ois = new
ObjectInputStream(fis);
User user = (User)ois.readObject();
ois.close();
return user;
}
public void setUser(User user) throws Exception{
String UserId = user.getUserId();
FileOutputStream fos;
fos = new FileOutputStream(StoragePath+
UserId +".ser");
ObjectOutputStream oos = new
ObjectOutputStream(fos);
oos.writeObject(user);
oos.close();
}
}
Come si può notare all'esterno il DAO espone
i metodi di salvataggio e lettura di una struttura dati
per la quale esso è stato pensato. L'entity bean
così come il semplice bean non devono quindi
preoccuparsi della apertura della connessione verso
il database. In una applicazione leg-germente più
complessa si potrebbero avere più DAO, ognuno
pensato per una struttura speci-fica: se si volesse
rendere le cose ancora più eleganti i vari DAO
potrebbero essere nascosti da un Factory che restituisca
il tipo voluto in base al parametro passato al metodo
di factory.
Inoltre un DAO potrebbe avere vari metodi di caricamento
dati, in modo da restituire vari tipi di DTO associati
alla stessa struttura dati, a seconda della vista in
esecuzione sul client: se ad esempio si è in
fase di caricamento della lista completa di tutti gli
utenti (magari con un siste-ma di paginazione) non è
necessario per ogni classe User caricare tutti i dati,
ma forse solo quelli importanti. Solo nel caso in cui
sul client si voglia vedere nel dettaglio i dati dell'utente
XYZ, si potrà caricare tutti i dati dell'utente
utilizzando quindi un altro metodo del DAO e completando
il caricamento del DTO corrispondente.
Figura 3 - Il sequenze diagram del pattern DAO
mostra in
sequenza le operazioni effettuate dai vari soggetto
coinvolti
Value
Object
Il pattern Value Object è per certi versi piuttosto
simile al DTO: esso rappresenta la struttura dati che
si vuole gestire e rappresenta un mapping diretto con
i dati presenti nel database (o nel sistema di storage
scelto). Gli Entity beans di per se offrono un sistema
VO, ma se non si desi-dera utilizzare tale framwork,
si potranno creare oggetti appositi che si interfaccino
con DTO per la comunicazione con il client e con DAO
per la persistenza. Di fatto un VO può essere
lui stesso un DTO ed essere inviato verso il client:
tale scelta però "porta" fin sul client
un oggetto pensato per essere gestito sui più
remoti strati server-side, limitando fortemente la separazione
dei contesti. Inoltre un DTO non dovrebbe avere niente
a che fare con il salvataggio scrittura, per cui non
dovrebbe sapere niente della esistenza di metodi corrispondenti
o della presenza di un DAO.
In ogni caso unire VO e DTO non è una grave mancanza,
e può essere considerata una soluzio-ne accettabile
se l'applicazione è complessivamente piuttosto
semplice.
Conclusione
Sulla scia delle ultime e recenti puntate dedicate alla
realizzazione di applicazioni multicanale, questo mese
è stato mostrato come aggiungere un po' di filosofia
J2EE alla architettura com-plessiva in modo da rendere
il tutto più elegante, scalabile e maggiormente
integrabile con il resto dei componenti. Adesso siamo
pronti per chiudere definitivamente con il canale web
e vedere come altri canali (il mobile o l'applicazione
stand alone ad esempio) possano integrarsi con lo strato
server side facendo tesoro in modo particolare dei pattern
e delle soluzioni viste questo mese.
Bibliografia
[MSHOP] - "MokaShop: realizzare una applicazione
J2EE multicanale", di Giovanni Puliti, MokaByte,
Febbraio 2002 - www.mokabyte.it/2002/02 e successivi.
[DAO] - "I pattern Data Access Object" di
Stefano Rossini e Luca Dozio, MokaByte 62, Apri-le 2002
[SF] - "Il pattern Session Façade"
di Stefano Rossini e Luca Dozio, MokaByte 64, Giugno
2002 - www.mokabyte.it/2002/06
[BD] - "Il pattern Business Delegate" di Stefano
Rossini e Luca Dozio, MokaByte 65, Luglio Agosto 2002
- www.mokabyte.it/2002/07
[LOCAL] "EJB 2.0: la Client Local API" di
Giovanni Puliti, MokaByte 70, Gennaio 2003 - www.mokabyte.it/2003/01
|