|
Il
problema del trasporto dati
Riconsiderando quanto analizzato negli articoli precedenti
([ARCH-1] e [ARCH-2]), 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.

Figura 1 - Una comune organizzazione a più
strati
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);

Figura 2 - Per ottenere tutte le informazioni
relative ad un articolo è ricavare uno per uno
gli attributi della entità "Articolo".
Se questo approccio è normalmente poco elegante
in uno scenario remoto aumenta il costo computazionale
con un degrado complessivo delle prestazioni
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 [UML]).
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.

Figura 3 - Porzione del modello di dati relativo
alle entità Article-Session-Writer
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 bean
public 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.

Figura 4 - L'utilizzo di un DTO permette di ridurre
drasticamente il numero di invocazioni remote
e semplifica l'aggregazione dei dati
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.

Figura 5 - Se i due DTO corrispondono a due versioni
compilate diverse il processo di
serializzazione e deserializzazione porta ad errori.
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 compilazione
articleDTO.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 [EJBDP], che per chiarezza
e sintesi è uno dei miei testi preferiti.
Bibliografia
[EJBDP] - "EJB Design Pattern" di Floyd Marinescu
Ed. Wiley
[ARCH-1] - "Architetture e tecniche di progettazione
EJB - I parte: l'architettura di una applicazione tipo"
di Giovanni Puliti . MokaByte 98 Luglio Agosto 2005
[ARCH-2] - "Architetture e tecniche di progettazione
EJB - II parte: il client ed i business delegate"
di Giovanni Puliti . MokaByte 100 Ottobre 2005
[UML] - "UML e ingegneria del software" di
Luca Vetti Tagliati. Ed. Hops-MokaByte
|