MokaByte 75 - Giugno 2003 

MokaShop il negozio online di MokaByte
Progettare applicazioni J2EE multicanale

VII parte -
completare l'architettura complessiva con qualche pattern J2EE in pił
di
Giovanni Puliti

Nel corso delle precedenti puntate abbiamo visto come realizzare una applicazione distribuita multistrato interfacciando direttamente lo strato web di presentazione con quello di business logic EJB. In particolare questa architettura prevede l'utilizzo del pattern MVC per la parte web e l'utilizzo di Enterprise Java Beans per la parte di business logic. Questo mese vedremo come migliorare l'architettura complessiva integrando alcuni pattern J2EE sia nella parte web che nella parte EJB: l'obiettivo é quello di migliorare l'architettura al fine di maggiorare il livello di disaccop-piamento fra i vari livelli e componenti. I pattern che verranno analizzati sono molto semplici e sono già stati presentati nei mesi scorsi nella serie di articoli di Stefano Rossini e Luca Dozio.


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:

  1. l'utente inserisce i propri dati userid e password
  2. 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

 

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