Architetture e tecniche di progettazione EJB

III parte: trasmissione dati tra strati con DTOdi

In una architettura multistrato uno dei problemi più importanti è legato alla comunicazione e della propagazione dei dati fra i vari strati. Il pattern DTO, che introduciamo questo mese, assolve brillantemente a questo compito: probabilmente uno dei pattern più semplici è al tempo stesso uno dei più importanti e denso di insidie.

In una architettura multistrato uno dei problemi più importanti è legato alla comunicazione e della propagazione dei dati fra i vari strati. Il pattern DTO, che introduciamo questo mese, assolve brillantemente a questo compito: probabilmente uno dei pattern più semplici è al tempo stesso uno dei più importanti e denso di insidie.

Il problema del trasporto dati

Riconsiderando quanto analizzato negli articoli precedenti ([2] e [3]), una buona organizzazione di una architettura enterprise è costituita normalmente di un stratificazione basata almeno sui seguenti livelli: strato client applicativo (esempio le actions di una applicazione Struts) strato client di comunicazione con il server (BD), strato server di session faà§ade, uno o più strati applicativi di sessione, strato data model (entity beans). Si veda la figura 1.

Questa organizzazione stratificata si basa principalmente sull‘assunzione che si possa utilizzare un qualche protocollo di invocazione remota tramite il quale si possa dar vita in modo semplice a una architettura distribuita multilivello.
Nel corso del tempo sono state presentate differenti soluzioni basate su piattaforme e tecnologie differenti: in questa serie di articoli (dedicate al DTO) si farà  riferimento a EJB quale tecnologia per la realizzazione di applicazioni distribuite; concettualmente le cose rimangono comunque valide se si prendessero in considerazione protocolli più anziani come RPC C/Unix, CORBA o anche semplicemente RMI.
Questi framework di invocazione infatti mettono a disposizione una serie di strumenti più o meno evoluti che permettono al programmatore di realizzare la propria architettura distribuita.
Le tecnologie più semplici o più anziane mettono in genere a dispozione solamente le funzionalità  base relative alla invocazione remota, mentre i servizi più avanzati come lookup di oggetti remoti, garbage collector distribuito, load balancing e pool di oggetti remoti, sicurezza e cosଠvia.
Quello che questi sistemi in genere non offrono (e d‘altronde non potrebbero) è un meccanismo di organizzazione dei dati durante il trasporto (vedendo l‘esempio che segue risulterà  immediatamente chiaro di cosa si stia parlando e quale sia la natura del problema da risolvere).
Per superare questo aspetto in genere un buon progettista ricorre alla programmazione per pattern e in particolare al pattern Data Transfer Object (DTO) che viene di solito impegnato assieme al DTOAssembler. Prima di comprendere come possa essere utilizzato tale pattern in tutte le sue sfumature, è utile capire quale sia il problema che esso risolve.
Si immagini la comunicazione che si instaura fra un client EJB ed il relativo session bean remoto; si ipotizzi che in un determinato momento della esecuzione del client, questo debba dover ricavare una serie di informazioni relative ad una qualche struttura dati remota. Per semplicità  ci rifaremo all‘esempio della applicazione MokaCMS che permette la manipolazione degli articoli del magazine MokaByte. Si può quindi immaginare che in un determinato momento il client debbe ricavare le informazioni di un articolo individuato per chiave primaria.
Tali informazioni possono essere attributi diretti dell‘articolo come il titolo, il sottotitolo il corpo dell‘articolo, ma anche relativi a strutture dati collegate all‘articolo, come l‘autore, o la sezione di appartenenza. Dato che si sta operando in uno scenario distribuito risulta ovvio come sia particolarmente scomodo (per il programmatore) e costoso (in termini di tempo di esecuzione) eseguire tante invocazioni remote per ottenere tutte queste informazioni.

public String getArticleTitle(String articleId);public String getArticleSubTitle(String articleId);public String getArticleBody(String articleId);public String getArticleWriterLastName(String articleId);

Molto più saggio sarebbe eseguire una sola invocazione remota dal client sul session bean e trasferire tutte le informazioni ottenibili in un solo passaggio; questa cosa può essere fatta in modo pratico ed elegante tramite il pattern Data Transfer Object.

Il DTO è un pattern molto semplice che permette di inserire tutte le informazioni necessarie al cliente in unico oggetto contenitore e di eseguire una sola spedizione dal server al client. Ovviamente questo ragionamento è valido anche per il flusso dati opposto dal client al server, quando si debba inviare delle informazioni allo strato di business logic per eseguire delle modifiche al modello dati.
In prima analisi il DTO è probabilmente uno dei pattern più semplici che si possa immaginare, ma spesso non si colgono quasi mai le insidie che esso nasconde, ne tantomeno le problematiche ad esso associato. Se infatti un DTO è un semplice contenitore di informazioni con il tempo ci si rende conto che vi sono una serie di aspetti di non immediata comprensione o soluzione: ad esempio per la definizione del DTO e per il suo popolamento è necessario decidere quante e quali informazioni si debbano inserire al suo interno, il livello di dettaglio delle informazioni stesse e soprattutto se e come riprodurre tramite un DTO la struttura dati del modello. Normalmente questi sono tutti aspetti legati alla particolare natura dello use case in esame e quindi risulta piuttosto ovvio quanto sia importante eseguire una approfondita analisi funzionale e una dettagliata raccolta dei cosiddeti use-case form (tipici della metodologia RUP, vedi [4]).
In questo articolo si affronteranno questi aspetti anche se una completa trattazione del problema richiederebbe un tempo infinito.
In questa trattazione si parlerà  spesso di entità  appartenenti al modello di dati da un lato e di oggetto DTO dall‘altro: questa associazione è inesatta dato che associa la vista della analisi ad oggetti (OOA) con un elemento della vista implementativa ad oggetto (OOP).
Tale relazione è usata qui per evidenziare la presenza di un modello astratto basato su entità  di vario tipo i cui dati saranno utilizzati per popolare gli oggetti contenitori DTO. In questo momento non è interessante sapere quale sarà  la scelta per l‘implementazione del modello di dati (entity bean, oggetti Hibernate, JDO o altro): in questa fase infatti tutta l‘attenzione è rivolta agli oggetti di trasferimento.

Il caso in esame: Article-Section-Writer

La struttura dati che si va a presentare e sulla quale verranno fatte le debite considerazioni è rappresentata nella figura 3.

Si tratta di una piccola parte del modello di dati della applicazione MokaCMS e rappresenta la relazione che si instaura fra un articolo (rappresentato dall‘entità  Article), i suoi autori (Writer) e la sezione di appartenenza (Section). Nella figura si possono anche notare le cardinalità  che si instaurano fra i vari oggetti. Un articolo è un oggetto il cui stato è descritto da una serie di attributi dei quali il campo body è forse il più importante: si tratta del corpo del testo dell‘articolo e potrebbe essere salvato su database tramite un campo blob, longtext o altro. Un articolo può appartenere solamente ad una sezione per volta. Un articolo può essere poi a sua volta associato a un numero arbitrario di autori.
Questa semplice struttura dati sarà  analizzata in modo da vedere che tipo di DTO creare e inviare al client, quali informazioni associare a un DTO e come legare fra loro tali oggetti al fine di riprodurre sullo strato client le informazioni presenti sul server.
Questa struttura dati, nel corso di questa serie di articoli dedicata al DTO, verrà  ripresa in esame più volte in modo da evidenziare alcuni importanti aspetti legati alla correttezza del modello.

DTO predefiniti o contenitori standard

Vi possono essere molte varianti sul tema principale, ma nella sua forma più semplice un DTO è un contenitore di informazioni, e in tal senso si possono fare fare due grosse scelte: utilizzare uno dei tanti oggetti container disponibili con il JDK (hashtable, properties, vector) oppure creare un oggetto ad hoc che riproduca le informazioni essenziali da spedire.
Si analizzerà  per primo il caso del DTO standard basato sull‘oggetto java.util.Hashtable.
Si supponga che all‘interno di un metodo remoto di un session bean si implementi l‘operazione di ricerca di un articolo, ovvero vengano effettuate quelle operazioni di ricerca (per chiave primaria) di un entity bean Article. Tale operazione restituirà  come risultato un puntatore alla interfaccia locale di ArticleBean; da tale interfaccia potranno essere ricavati tutti i dati per popolare il DTO che verrà  poi passato (tramite Business Delegate, visto nell‘articolo precedente).

// esegue una operazione di ricerca per chiave primaria sulla entità  articolo// e ricava la interfaccia locale dell‘entity beanpublic ArticleDTO articleFindByPrimaryKey(String id) throws EJBException {try {ArticleLocal articleLocal = articleLocalHome.findByPrimaryKey(id));Hashtable articleDTO = new hashtable();articleDTO.put("articleName", articleLocal.getName());articleDTO.put("abstractText", articleLocal.getAbstractText());articleDTO.put("notes", articleLocal.getNotes());articleDTO.put("title", articleLocal.getTitle());articleDTO.put("subTitle", articleLocal.getSubTitle());articleDTO.put("introduction", articleLocal.getIntroducion());articleDTO.put("body", articleLocal.getBody());articleDTO.put("id", articleLocal.getId());articleDTO.put("status", articleLocal.getStatus());return articleDTO;}catch (ObjectNotFoundException onfe) {logger.debug("nessun articolo trovato con Id=" + id);return null;}catch (Exception e) {throw new EJBException(e.getMessage());}}

Sullo strato client si potranno utilizzare i dati contenuti nel DTO per visualizzare le informazioni relative all‘articolo appena cercato. Per esempio:

ArticleDTO articleDTO = businessDelegate.articleFindByPrimaryKey("123-625-145");String title = (String) articleDTO.get("title");String subTitle = (String) articleDTO.get("subTitle");

In questo caso si ricavano il titolo e sottotitolo dell‘articolo.

Si può notare subito che, a fronte di una grande semplicità  nella struttura e gestione del DTO, con questo tipo di approccio si va incontro ad un grave problema: la completa assenza di controllo sui nomi e sui tipi.
Per esempio se sullo strato client si compiesse il seguente errore

String subTitle = (String) articleDTO.get("subtitle");

sbagliando la sintassi del nome del campo presente nel DTO (tutto minuscolo invece di "subTitle"), questo non potrebbe essere in alcun modo controllato in fase di compilazione e per certi versi nemmeno in fase di esecuzione. Discorso analogo se uno degli attributi contenuti nell‘articolo fosse di tipo diverso da stringa si otterrebbe in tempo di esecuzione (e non prima) una ClassCastException.

Purtroppo questo tipo di scenario è piuttosto frequente, specie se un DTO viene usato da team di sviluppo differenti: il team A sviluppa la parte web, il team B sviluppa o ha sviluppato in passato (caso peggiore perchà© introduce errori dovuti a dimenticanze o cattiva documentazione) la parte EJB.
Questo genere di problemi in genere sono talmente pericolosi e fastidiosi che difficilmente un DTO di questo tipo viene visto con simpatia. Verrebbe da chiedersi perchà© possa essere passata nella mente del Creatore Universale di DTO una idea tanto malsana.
Il motivo è molto semplice: si pensi al caso (molto frequente) in cui lo sviluppo dei vari strati applicativi (web e EJB) sono portati avanti di pari passo da team di sviluppo differenti.
Normalmente il gruppo che sviluppa la parte EJB si preoccupa di produrre anche i vari DTO che poi verranno passati, insieme ai business delegate e RMI stub, in un jar il quale verrà  poi inserito nel classpath dello strato client.
Se non si utilizzassero DTO standard ma custom (vedi oltre) per ogni modifica alla classe DTO o anche semplicemente per ogni ricompilazione, il compilatore associa un differente SerialVersionUID al .class. E questo obbliga a dover ridistribuire a tutti gli applicativi client il nuovo .class dentro un .jar ricostruito per l‘occasione.

Nel caso di applicazioni web spesso i vari ambienti di sviluppo e deploy operano cache delle librerie e quindi questo scenario porta spessissimo a inspiegabili problemi: non è raro vedere programmatori alle prime armi con J2EE sull‘orlo della pazia per incomprensibili errori che saltano fuori il lunedi mattina, dopo che il venerdi sera precedente prima della chiusura tutto funzionava perfettamente.
Personalmente sono contrario all‘uso di DTO standard perchà© il problema della assenza di type e name-cheking sia troppo grave e troppo in antitesi con la filosofia base di Java. Nel caso in cui si vogliano utilizzare container standard è essenziale utilizzare un elevato controllo sul codice prodotto e produrre una accurata documentazione (in particolare sui nomi degli attributi), attivando al contempo una dettagliata logica di log (per evidenziare immediatamente ogni piccolo errore).

Il DTO Custom

Contrariamente al caso precedente il DTO custom è rappresentato da una classe che viene scritta appositamente per contenere tutte le informazioni della classe alla quale è associato. Ad esempio riprendendo il caso dell‘articolo

public class ArticleDTO implements java.io.Serializable {public final static String STATUS_READY="READY";public final static String STATUS_NOT_READY="NOTREADY";protected String name;protected String abstractText;protected String notes;protected String title;protected String status;protected String subTitle;protected String introducion;protected String body;protected String id;private String serialName;private String keywords;public ArticleDTO() {super();}public ArticleDTO(String name, String abstractText, String notes, String title, String subTitle, String introducion, String body, String id, String status, String serialName, String keywords){this.name=name;this.abstractText=abstractText;this.notes=notes;this.title=title;this.subTitle=subTitle;this.introducion=introducion;this.body=body;this.id=id;this.status=status;this.serialName=serialName;this.keywords=keywords;}

Riconsiderando il caso del metodo del session bean dove si esegue la ricerca di un article, le operazioni relative al popolamento del DTO sono sotto lo stretto controllo del compilatore:

public ArticleDTO articleFindByPrimaryKey(String id) throws EJBException {try {ArticleLocal articleLocal = articleLocalHome.findByPrimaryKey(id);ArticleDTO articleDTO = new ArticleDTO();articleDTO.setTitle(articleLocal.getTitle());// se si effettua un errore di naming sulle proprietà  del DTO// o del bean il compilatore blocca la compilazionearticleDTO.setsubtitle(articleLocal.getsubTitle());return articleDTO;}catch (ObjectNotFoundException onfe) {logger.debug("nessun articolo trovato con Id=" + id);return null;}catch (Exception e) {throw new EJBException(e.getMessage());}}

Tornando per un momento ad analizzare meglio il DTO custom si possono subito evidenziare alcune importanti cose: la prima e forse più importante è che un DTO, dovendo passare i vari strati applicativi e funzionare come parametro di input-output di invocazioni RMI, deve essere serializzabile. Se questo era implicito con l‘uso di un DTO standard, in questo caso deve essere esplicitamente dichiarato tramite l‘implementazione della interfaccia Serializable.
Altra cosa importante è la presenza degli attributi corrispondenti agli attributi della entità  Article: si possono notare che sono tutti non pubblici (e quindi sono sempre accessbili tramite corrispondenti metodi accessori setXXX() e getXXX()) e che in questo caso particolare essi sono definiti come protected, non private.
Il motivo di questa scelta è dovuto alla necessità  che nasce a volte di creare non un semplice DTO per tutte le stagioni, ma una vera e propria gerarchia in cui la classe base svolge il compito di DTO elementare e i discendenti implementano compiti più evoluti o strutturati. Si avrà  modo di affrontare questo aspetto in seguito.
Interessante anche notare la presenza di un costruttore che riceve dall‘esterno tutti i singoli attributi per la valorizzazione: la cosa in realtà  è piuttosto scomoda e si sarebbe potuto pensare di seguire una strada più object oriented, ad esempio passando la interfaccia local di ArticleBean al costruttore del DTO

public ArticleDTO(ArticleBeanLocal articleBeanLocal){this.name= articleBeanLocal.getName();....}

In questo modo dall‘esterno l‘istanziazione del DTO risulta molto più semplice e gestibile. Questa soluzione però viola la filosofia di base di un data transfer object: tale pattern infatti dice che l‘oggetto di trasporto deve essere solo un semplice fattorino e non deve sapere nulla nà© della destinazione nà© tanto meno della partenza dei dati.
Se al costruttore si passa un oggetto parente del framework EJB si lega tale DTO sia al package javax.ejb che al package specifico del bean Article. Design pessimo anche perchà© su un eventuale strato client si sarebbe obbligati a importare tali classi.
La soluzione è quella di utilizzare una classe apposita che si preoccupa di assemblare un DTO partendo dalla entità  prescelta:

public class ArticleDTOAssembler {public static ArticleDTO createDto(ArticleLocal articleLocal) {ArticleDTO articleDTO = new ArticleDTO();if (articleLocal != null) {articleDTO.setName(articleLocal.getName());articleDTO.setAbstractText(articleLocal.getAbstractText());articleDTO.setNotes(articleLocal.getNotes());articleDTO.setTitle(articleLocal.getTitle());articleDTO.setSubTitle(articleLocal.getSubTitle());articleDTO.setIntroducion(articleLocal.getIntroducion());articleDTO.setBody(articleLocal.getBody());articleDTO.setId(articleLocal.getId());articleDTO.setStatus(articleLocal.getStatus());}return articleDTO;}

L‘oggetto DTOAssembler è quindi un oggetto che si lega da un lato al neutro DTO dall‘altro al modello di dati specifico. Verrebbe da pensare che si potrebbe senza commettere errori o implementare un design errato, creare un DTOAssembler nel caso in cui il modello dati sia implementato da entity beans, uno per JDO e uno Hibernate. In aggiunta a questa considerazione un DTOAssembler può contenere al suo interno varie versioni del metodo di factory in modo da permettere la creazione di diverse forme di DTO (leggeri o pesanti) in accordo con le reali necessità  del client. Si parlerà  in modo approfondito di questi aspetti in seguito.
Riconsiderando il codice visto in precedenza, il metodo del session bean che cerca un article e crea il DTO corrispondente potrebbe diventare una cosa del tipo

public ArticleDTO articleFindByPrimaryKey(String id) throws EJBException {try {return ArticleDTOAssembler.createDTO(articleLocalHome.findByPrimaryKey(id));} catch (ObjectNotFoundException onfe) {logger.debug("nessun articolo trovato con Id=" + id);return null;}catch (Exception e) {throw new EJBException(e.getMessage());}}

Che ovviamente risulta essere molto più pulita e semplice da leggere e scrivere.

La scelta dei nomi

Una ultima considerazione veloce,che si riallaccia al problema della separazione dei nomi e delle classi appartenenti ai vari strati. E‘ utile infatti adottare un approccio pulito e conservativo anche per la scelta dei nomi dei package. Se le classei appartenenti allo strato client-web sono inserite in un package che ha nomi del tipo

com.mokabyte.mokacms.web

e la corrispondente parte server EJB

com.mokabyte.mokacms.ejb

allora è buona cosa che il DTO, non appartenendo concettualmente nà© al client nà© al server,
sia inserito in un package dal nome

com.mokabyte.mokacms.util

Conclusioni

Questo mese abbiamo introdotto i concetti base del pattern DTO. Questo è probabilmente uno dei più semplice del panorama J2EE, ma nasconde importanti considerazioni ed inside non sempre evidenti. Creare un buon set di DTO che siano utili ma al tempo stesso efficaci e performanti è un compito la cui esecuzione spesso richiede una buona dose di esperienza e pragmatismo unitamente ad una ottima conoscenza del paradigma ad oggetti, della piattaforma J2EE ma soprattutto del dominio del problema.
Spesso una cattiva progettazione del DTO e del DTOAssembler introducono rallentamenti del sistema non prevedibili.
Anche se può sembrare una considerazione banale è bene tenere a mente più che mai, per creare dei buoni DTO e usarli con profitto è necessaria una una approfondita analisi dei vari use case e del domain model.
Nel prossimo articolo si parlerà  delle relazioni che si instaurano fra i vari DTO (per esempio fra article e section) e delle differenti tipologie di oggetti (leggeri che portano solo l‘essenziale, pesanti che portano tutto, misti che assemblano informazioni eterogenee).
Una ultima nota conclusiva: il testo di riferimento che deve diventare "il testo di riferimento" per quanto concerne gli aspetti affrontati in questi articoli è senza dubbio il [1], che per chiarezza e sintesi è uno dei miei testi preferiti.

1Floyd Marinescu"EJB Design Pattern" Wiley2Giovanni Puliti"Architetture e tecniche di progettazione EJB - I parte: l‘architettura di una applicazione tipo" MokaByte 98 Luglio Agosto 20053Giovanni Puliti"Architetture e tecniche di progettazione EJB - II parte: il client e i business delegate". MokaByte 100 Ottobre 20054Luca Vetti Tagliati

Condividi

Pubblicato nel numero
102 dicembre 2005
Giovanni Puliti lavora come consulente nel settore dell’IT da oltre 20 anni. Nel 1996, insieme ad altri collaboratori crea MokaByte, la prima rivista italiana web dedicata a Java. Da allora ha svolto attività di formazione e consulenza su tecnologie JavaEE. Autore di numerosi articoli pubblicate sia su MokaByte.it che su…
Articoli nella stessa serie
Ti potrebbe interessare anche