Introduzione
Nel precedente articolo abbiamo raccontato che cosa è il Canonical Data Model (CDM) e abbiamo dato indicazioni per comprenderlo e permettere di individuare il giusto contesto in cui impiegarlo. I contenuti dell’articolo erano prevalentemente rivolti a un pubblico di architetti del software, o comunque a persone che hanno delle responsabilità decisionali sull’architettura software di un qualche prodotto.
In questo articolo, invece, ci concentreremo più sul punto di vista dello sviluppatore, in particolare quello che scrive codice in Java lavorando in un team e che deve implementare dei moduli di un prodotto complesso. Immaginiamo che per il prossimo progetto il software architect abbia deciso che si dovrà seguire il pattern Canonical Data Model, poichè si dovranno integrare tra di loro diversi moduli sviluppati dai membri del team e/o l’intera applicazione dovrà scambiare informazioni con applicazioni esterne.
Se siete sfortunati però, il vostro architetto non è un tipo che vi segue passo passo nel lavoro mentre applicate le sue direttive. Invece di maledire noi per aver dato l’idea malsana al vostro architetto di spingervi verso una strada sconosciuta, proseguite la lettura e troverete indicazioni per cominciare in modo rapido, andare a regime velocemente con l’uso del pattern e quindi potervi concentrare sugli aspetti più importanti in poco tempo, vale a dire la modellazione del CDM.
Primi passi
Ok, prima di continuare, aprite un tab del vostro browser sull’indirizzo
https://github.com/cristcost/sensormix
SensorMix [1] è un’applicazione modulare di esempio, fatta da noi riprendendo le metodologie che adoperiamo nell’ambito professionale; la useremo come riferimento e caso d’uso per spiegare le tecniche che sono illustrate nell’articolo.
Il CDM come modulo, in pratica.
Seguendo il nostro approccio, si sviluppa il CDM come modulo, ossia come progetto Java che viene compilato e distribuito in un JAR. Il fatto che il modulo sia un JAR autonomo è importante perchè il CDM dovrà essere riutilizzato da vari altri progetti in maniera indipendente.
In SensorMix il progetto del CDM lo abbiamo chiamato sensormix-datamodel-api: per quanto riguarda il nome del progetto, abbiamo preferito usarne uno che non fa riferimento al pattern, ma piuttosto al fatto che il progetto contiene un data model e al fatto che è una libreria ossia un insieme di Application Programming Interfaces (API) da riutilizzare in altri progetti.
Va detto poi che il progetto è realizzato con Maven, che si sposa ottimamente con un approccio modulare e consigliamo caldamente di adottarlo; tuttavia la maggior parte dei princìpi possono essere applicati anche se si usano altri strumenti di sviluppo Java.
È bene comunque stabilire una gerarchia di relazioni tra i moduli: il modulo del CDM deve stare a livello 1 come abbiamo fatto per SensorMix (figura 1).
In SensorMix abbiamo adottato i pattern di applicazioni modulari Java [2]. Nella figura precedente, in seguito a una progettazione delle dipendenze che segue il pattern Levelize Modules, si può vedere come le relazioni tra i moduli seguona i pattern di Acyclic Relationship. Si noti come tutti i moduli di secondo livello siano indipendenti tra loro e abbiano in comune solo la dipendenza dal Canonical Data Model.
Il primo passo da fare per cominciare lo sviluppo è quindi semplice: create un nuovo progetto vuoto usando Maven o il vostro IDE, date al progetto un nome significativo e preparatevi a popolarlo con le classi, interfacce ed enumerazioni del CDM come vi spiegheremo nel prossimo paragrafo.
Modellare le classi del CDM
Al posto di XML Schema o altri linguaggi di descrizione di contenuti, nel nostro approccio basato su Java il CDM è modellato come un insieme di classi, enumerazioni e, per un caso specifico, interfacce Java.
Il CDM deve rappresentare l’informazione scambiata e non deve contenere logica; per questo motivo si usano solamente classi che rispettino le regole dei Plain Old Java Object (POJO): ossia le classi sono composte solo di campi, che a loro volta possono essere soltanto POJO o tipi primitivi, e gli unici metodi presenti sono i getter e i setter.
I POJO del CDM non estendono classi che non siano a loro volta altri POJO del CDM: l’ereditarietà non va usata come modalità per estendere le funzionalità di un oggetto dotando un POJO di comportamenti che non deve avere, ma solo per catturare relazioni gerarchiche dell’informazione e anzi incoraggiamo a usarla se è per questo scopo. Non utilizzate però le interfacce: infatti una interfaccia definisce dei metodi e non dei dati. Ma qui vogliamo modellare l’informazione da scambiare, e quindi una interfaccia avrebbe poco senso. Le uniche eccezioni possono essere le interfacce come java.io.Serializable, che infatti non definisce alcun metodo.
Piuttosto quando dovete modellare qualcosa di astratto, utilizzate il modificatore di classe abstract in modo da identificare i POJO che non volete siano creati e che rappresentano solo una gerarchia. Visto che in Java le enum definiscono delle classi, anche queste possono essere utilizzate, e anzi consigliamo di farlo quando un’informazione può assumere solo un insieme di valori prestabiliti.
Evitate nell’intero CDM di usare java.lang.Object come tipo di un campo di POJO: questo diventerebbe un punto di estensione incontrollato e a cui si potrebbe assegnare una classe qualsiasi. In pratica poi, avreste grossi problemi a gestire la serializzazione di campi di questo tipo.
Per le collezioni, limitate il vostro CDM a usare soltanto array oppure soltanto java.util.List<> specificando un tipo generico che sia a sua volta un POJO, evitando quanto detto sopra riguardo alla classe Object. Evitate anche le mappe e preferite a queste delle liste lineari nelle quali gli oggetti memorizzati hanno una chiave come campo dell’oggetto. Questo perchè quando arriveremo a serializzare il nostro CDM, preferiamo che le chiavi identifichino un campo e, usando le mappe Java, si avrebbero ambiguità.
Infine, ci sono pareri discordanti sull’uso di annotazioni sui POJO, ma noi siamo favorevoli e anzi ne abusiamo, non solo adottando le annotazioni JaxB e JaxWS per esportare il nostro CDM verso i Web Service come vedremo sotto, ma costruiamo anche nostre annotazioni che adoperiamo per assegnare una semantica alle classi del modello.
La modellazione secondo queste regole vi condurrà a realizzare un formato che ha capacità simili all’XML o al JSON ma, a differenza del primo, essendo fatta in Java è per noi un linguaggio più consono per lavorare e, a differenza del secondo, è tipizzato e quindi più facile da usare quando sviluppiamo.
Interfacce per modellare servizi
Abbiamo detto che il CDM è fatto anche di interfacce, quando possiamo usarle allora? Nel nostro approccio, le interfacce si usano per modellare i “servizi” che scambiano le informazioni, e che quindi rappresentano le funzionalità offerte da un endpoint di una applicazione.
Per distinguere meglio il concetto di servizio dall’informazione scambiata, noi dividiamo solitamente il Canonical Data Model in due sub-package, uno chiamato model e l’altro service: nel primo raggruppiamo tutte le classi che rappresentano le informazione del dominio del prodotto, mentre nel secondo raggruppiamo le interfacce appunto dei servizi insieme alle altre classi che servono per scambiare le informazioni del model. Gli oggetti inseriti nel package model devono essere indipendenti da quelli inseriti nel package service e questo ci permette di tenere più pulito il CDM che si realizza.
L’esempio del web
Pensiamo a un esempio: il web. Nel web, l’HTML e l’HTTP sono proprio degli standard e quindi un CDM non è necessario: non ci si aspetta infatti un CDM quando si ha a che fare con un unico formato specifico, ma quando si devono integrare applicazioni che trattano lo stesso tipo di informazioni ma con formati diversi. Ma il web è un buon esempio perchè tutti conosciamo come funziona e il browser e il web server sono proprio due applicazioni che si integrano scambiando pagine web in HTML.
Se si volesse realizzare un CDM del web, si potrebbe cominciare creando due package:
org.example.theweb.cdm.model org.example.theweb.cdm.service
L’informazione principale scambiata è la risorsa web: modelliamo questa come un oggetto chiamato WebResource che aggiungiamo al package model. La pagina web è a sua volta una WebResource e la modelliamo con la sottoclasse WebPage.
Il servizio principale è il Web Server. Limitiamoci a modellare la funzionalità di recuperare una pagina con una GET da un percorso specifico, con supporto all’invio di parametri tramite query string. Allora si può farlo creando un’interfaccia Java chiamata WebServer che abbia il metodo con la seguente firma:
public WebResource get(String pagePath, List queryString);
Il risultato finale, ovviamente semplificato, sarà quindi la creazione di una struttura di classi come in figura 3.
Riguardo all’oggetto Parameter che viene passato nel metodo get dobbiamo fare due osservazioni.
La prima è che questa informazione è funzionale alla esecuzione del servizio, e non fa parte del modello della pagina web: per questo la inseriamo nel package org.example.theweb.cdm.service. Questa suddivisione risulta più facile da comprendere se si pensa anche che la query string è definita nelle RFC di HTTP e URI, che definiscono standard usati dal web come protocollo di trasferimento, e non nello standard HTML.
La seconda osservazione è che, come abbiamo detto in precedenza, si può vedere che non abbiamo usato una mappa Java, ma una lista di oggetti Parameter, che abbiamo modellato semplicemente con due campi di tipo stringa, uno di nome key e l’altro di nome value. Così facendo, l’informazione trasportata è equivalente a quella di una mappa.
Esempio: modellazione del CDM di SensorMix
La modellazione delle classi del CDM è l’aspetto chiave per ottenere un’integrazione di successo ma, dipendendo fortemente dal dominio applicativo, risulta difficile dare delle regole generali che siano applicabili in ogni circostanza. Esaminiamo quindi un esempio più rappresentativo, il CDM realizzato per SensorMix, e cerchiamo di intuire i ragionamenti che stanno dietro queste scelte.
L’applicazione SensorMix cattura informazioni da sensori, integra sensori basati su Android, Arduino e iOS, e fornisce i dati raccolti ad una web application realizzata con GWT così come rappresentato in figura 4.
Su una board Arduino abbiamo installato dei sensori di luminosità e temperatura. Su iOS usiamo la videocamera per contare tramite videoanalisi quanti volti sono presenti nella scena inquadrata. Infine su Android catturiamo la posizione GPS, la potenza del segnale delle antenne WiFi rilevate e la lettura di un tag NFC.
Le informazioni scambiate dai sensori presi in considerazione sono significativamente eterogenee ma hanno in comune alcuni aspetti: il più importante è che, in ogni caso, vengono scambiati con SensorMix dei campionamenti effettuati in un certo istante. Per questo cominciamo la modellazione intorno al concetto di “sample” ossia in italiano il “campione”.
Il “model”
Creiamo quindi la classe che rappresenta questo concetto: la chiamiamo AbstractSample perchè intendiamo derivarla in sottoclassi specifiche a seconda dell’utilizzo.
La classe AbstractSample contiene un Date per memorizzare il timestamp del campione. Oltre a questo abbiamo aggiunto due campi di tipo String che possano memorizzare l’ID del sensore a cui il campione appartiene, e un tipo che permetterà logiche di aggregazione sulle informazioni provenienti da sensori diversi ma rappresentanti la stessa misura.
AbsrtactSample non contiene l’informazione misurata, e per questo la classe si specializza in 4 sotto-classi:
- NumericValueSample, utilizzata per i campioni di temperatura, di luminosità e il rilevamento del numero di volti inquadrati dalla fotocamera;
- StringValueSample, utilizzata per i tag NFC;
- WifiSignalSample, utilizzata per la potenza del segnale WiFi;
- PositionSample, utilizzata per la posizione GPS.
Si noti come le prime due siano molto generiche, mentre invece le altre siano molto specifiche: questa flessibilità, che permette di spaziare tra diversi livelli di generalizzazione, ci è stata molto utile poichè ci ha permesso di adattare il modello dati alle esigenze di sviluppo dell’applicazione. Consigliamo di cercare di mantenere il modello semplice e generale, evitando di far crescere il numero di classi; ma laddove ci siano bisogni particolari, si riesce a gestirle abbastanza bene modellandole dettagliatamente in questo modo.
Il “service”
Per modellare il servizio, analizziamo cosa questo deve fare nei confronti delle applicazioni esterne, ossia nei confronti dei sensori e della web application. Esaminiamolo da un punto di vista focalizzato ai flussi di scambio di informazioni:
- gestione dei campioni: i sensori inviano i campioni a SensorMix, la Web Application accede ai dati raccolti;
- gestione dei sensori: la Web Application accede all’anagrafica dei sensori, la Web Application modifica l’anagrafica dei sensori;
- reportistica: la Web Application ottiene dati aggregati dei campioni dei sensori.
Le funzionalità non sono molte e può bastare raggruppare tutto in un’unica interfaccia Java che chiamiamo SensormixService. Dalle voci dell’elenco precedente si modellano direttamente i metodi di cui questa si compone.
Vediamo come modellare la gestione dei campioni.
Per la prima funzionalità aggiungiamo un metodo a grana grossa, ossia che permetta a un sensore di registrare un insieme di AbstractSample con un’unica chiamata, avente la seguente firma:
void recordSamples(List samples);
La web application mostrerà in tabella i campioni: aggiungiamo quindi un metodo che le permetta di recuperarli, tenendo in considerazione la possibilità di limitare i risultati nel tempo e nel numero:
List getSamples(String sensorId, String sampleType, Date from, Date to, Long limitFrom, Long limitCount);
Per implementare correttamente l’impaginazione dei risultati, aggiungiamo al servizio anche un metodo per contare il numero di campioni totali in un arco temporale:
long countSamples(String, String, Date, Date);
Infine aggiungiamo altri due metodi per recuperare le liste di tipologie e di ID di sensori, immaginando di voler permettere che l’utente scelga questi da un menu a tendina oppure di presentare un menu di navigazione sul browser che sia basato su questi elenchi:
List listSamplesTypes(); List listSensorsIds();
Per le altre funzionalità abbiamo aggiunto al servizio i metodi registerSensor e getSensors per gestire l’anagrafica dei sensori, e il metodo getSampleReport per l’accesso ai dati aggregati.
In generale le classi possono però diventare numerose e la modellazione del CDM può diventare più complessa, ma si può e si raccomanda di usare le tecniche di organizzazione dei progetti Java per gestire in maniera ordinata questo insieme di informazioni strutturandole ad esempio in più package o più progetti rispetto a come vi abbiamo presentato nell’esempio. In tal caso, mantenete la distinzione tra i package dei servizi e quelli di modello, ossia ricordate che questi ultimi che sono indipendenti dai primi: dentro alle classi di package model non si deve trovare nessun import di classi che provengono dai package service!
Comunicare utilizzando il CDM
Si noti a questo punto che la nostra modellazione è stata effettuata in modo astratto: abbiamo catturato appunto solo le astrazioni e le abbiamo modellate in puro Java, senza tenere in considerazione gli aspetti di comunicazione, di “sterilizzazione” dei dati o dei protocolli. Questa è l’anima del CDM è così deve essere, e in particolare il processo è stato più “agile” proprio perchè fino a ora abbiamo ignorato questi aspetti, concentrandoci su quelli di “business” del prodotto.
Ma come possiamo fare adesso per trasmettere effettivamente queste informazioni? Vale la pena citare tre opzioni significative:
- usare il CDM direttamente in ambiente OSGi;
- usare i web service annotando il nostro datamodel con JaxWS e JaxB;
- usare Apache Camel per trasformare dal CDM ad altri formati.
La più interessante e flessibile è la terza opzione, ma purtroppo l’argomento è complesso e richiederebbe un intero articolo. Se volete farvi una idea però, potete intanto dare una occhiata al progetto del modulo sensormix-integration-bundle, il quale adotta Camel per implementare il pattern Message Translator da Android, iOS e Arduino verso il nostro CDM fatto in Java. Vediamo qui di seguito le altre due strade.
OSGi
In ambiente OSGi si eseguono applicazioni modulari [3] [4] composte da “bundle” che comunicano tra di loro all’interno di una JVM attraverso gli OSGi Services. Questi si basano su interfacce Java; e il CDM realizzato come vi abbiamo raccontato si presta bene per questo tipo di comunicazione.
Se stiamo sviluppando in questo ambiente, usare il modulo CDM così creato è semplice e basta configurare un plugin di Maven in modo da renderlo compatibile con le specifiche dei “bundle”.
Piuttosto che trasformare l’intero progetto in un bundle, preferiamo usare il maven-bundle-plugin per generare solamente il manifest compatibile con OSGi usando il goal manifest del suddetto plugin. Configurate il plugin nel pom.xml nel modo seguente:
<plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <version>${bundle.plugin.version}</version> <extensions>true</extensions> <configuration> <instructions> <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName> <Bundle-Description>${project.description}</Bundle-Description> <!-- Using default for <Export-Package> --> <!-- Using default for <Import-Package> --> </instructions> </configuration> <executions> <execution> <id>bundle-manifest</id> <phase>process-classes</phase> <goals> <goal>manifest</goal> </goals> </execution> </executions> </plugin>
Il plugin così configurato genera il manifest utilizzando e impostazioni di default per Export-Package, ossia non saranno esportati il package di default nei package che contengono ‘impl’ o ‘internal’ al loro interno. Vi raccomandiamo di attenervi a questa convenzione ed evitare nel modulo del CDM di usare package con queste keyword, in quanto per sua natura tutti i POJO e le interfacce devono essere esportate.
Il modulo è quasi pronto, ma per assicurarvi che il manifest generato dal plugin sia usato correttamente per creare il JAR, aggiungete anche la seguente configurazione per il maven-jar-plugin:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestFile> ${project.build.outputDirectory}/META-INF/MANIFEST.MF </manifestFile> </archive> </configuration> </plugin>
Usare il CDM con i Web Service attraverso l’approccio Java-First
Nella prima parte dell’articolo abbiamo detto che noi utilizziamo l’approccio Java-first, il che per essere precisi implica l’adozione dei web service, ma fino ad ora abbiamo tenuto questo aspetto in disparte.
In SensorMix alla fine i web service non li abbiamo nemmeno usati, ma in ambito professionale ci permettono di comunicare con un vasto insieme di applicazioni sviluppate da nostri partner e sono per il nostro prodotto lo strumento di comunicazione più importante. Ci garantiscono compatibilità con altre piattaforme importanti come .NET, nella quale rigeneriamo le classi del CDM in C# a partire dai WSDL e dai file XML Schema.
Se avete seguito correttamente le regole per creare le classi del CDM, allora per ottenere i WSDL vi basterà aggiungere le annotazioni JaxB e JaxWS alle classi e configurare un plugin di Maven per generare il WSDL. E se avete fatto errori di modellazione, quando arrivate a generare quest’ultimo otterrete utili suggerimenti per individuarli.
Cominciamo con il configurare Maven utilizzando il plugin per la generazione di Apache CXF:
<plugin> <groupId>org.apache.cxf</groupId> <artifactId>cxf-java2ws-plugin</artifactId> <version>${cxf.plugin.version}</version> <executions> <execution> <id>process-core</id> <phase>process-classes</phase> <configuration> <className> com.google.developers.gdgfirenze.service.SensormixService </className> <outputFile> ${basedir}/target/generated/src/main/resources/sensormix.wsdl </outputFile> <genWsdl>true</genWsdl> <verbose>true</verbose> <argline>-createxsdimports</argline> </configuration> <goals> <goal>java2ws</goal> </goals> </execution> </executions> </plugin>
Il plugin così configurato genera il WSDL a partire dalla interfaccia Java SensormixService.java, e insieme ad esso genera i file XSD associati.
Noi utilizziamo diverse annotazioni per personalizzare il WSDL e gli XML Schema che verranno generati. I POJO del data model sono annotati con JaxB, mentre le interfacce dei servizi sono annotate sia con JaxB che JaxWS. Inoltre annotiamo il file package-info.java per personalizzare il namespace XML e ottenere degli XML Schema organizzati come desideriamo.
In questo articolo non c’è spazio per fare un corso di come usare JaxB e JaxWS, per esempi dei quali vi invitiamo ad aprire il progetto del modulo sensormix-datamodel-api, ed esplorare come abbiamo trattato le sue classi.
Esempio: ServiceMix Web Service
SensorMix ha un modulo che usa insieme sia l’approccio OSGi che l’approccio Web Service. Questo è il sotto-progetto sensormix-datawebservice-bundle.
Questo bundle usa Karaf così come descritto in [3] e [4] per connettersi a un SensormixService tramite servizi OSGi e lo ripubblica come web service grazie alle annotazioni che abbiamo aggiunto. Tutto questo si ottiene senza scrivere una riga di Java, ma utilizzando invece Spring con le estensioni per OSGi per CXF. Se aprite il file beans-osgi.xml, l’unico file sorgente del progetto, trovate due dichiarazioni speciali di Beans che integrano le due tecnologie:
<osgi:reference id=“sensormixService” interface=“com.google.developers.gdgfirenze.service.SensormixService” timeout=“30000” cardinality=“1..1” />
<jaxws:endpoint id=“sensormixWebService” implementor=“#sensormixService” address=“/sensormixWebService” endpointName =“tns:SensormixWebServiceEndpoint” serviceName=“tns:SensormixWebService” xmlns_tns=“http://developers.google.com/gdgfirenze/webservice” />
La prima dichiarazione, crea un bean “client” che si connette a un servizio OSGi che implementa l’interfaccia SensormixService; la seconda dichiarazione invece crea un Web Service che come implementazione usa il bean precedente: installando questo semplice bundle in Karaf potete usare il servizio OSGi tramite SOAP.
Questo è solo un esempio di utilizzo ed è possibile fare molto altro con questo CDM, sopratutto quando entra in gioco un framework come Camel.
Conclusioni
Nella modellazione del CDM e più in generale nella definizione delle interfacce di un sistema, studiare e individuare quali sono gli scambi di informazione necessari tra le applicazioni, e poi modellarli raggiungendo la giusta sintesi, è la parte più difficile.
L’approccio che vi abbiamo illustrato aiuta noi sviluppatori nel fare questo lavoro: utilizzando il Java proprio per catturare i dettagli, possiamo eseguire l’analisi con più attenzione e, invece di un tool di disegno UML, possiamo adoperare direttamente il nostro ambiente di sviluppo preferito. In Eclipse o in altri IDE abbiamo a disposizione strumenti utili per fare refactoring al volo quando cambiamo idea, e quindi tutta la modellazione si svolge con grande agilità mantenendo soprattutto coerenza. E i diagrammi UML che avete visto in questo articolo sono tutti generati a partire dal Java, usando ObjectAid [5] per Eclipse, un semplice plugin che disegna il diagramma semplicemente trascinandoci dentro i file Java.
Per ottenere delle integrazioni tra applicazioni che siano gestibili e semplici da comprendere, flessibili alle modifiche e versatili verso formati dati e protocolli eterogenei, è necessario approfondire la conoscenza dei framework di comunicazione e soprattutto fare esperienza per maturare le proprie abilità di modellazione.
Le indicazioni contenute in questo articolo sono utili per orientarsi ed è l’approccio che noi utilizziamo e che raccomandiamo anche a voi.