Introduzione
Nel nostro team ci sono alcune parole molto ricorrenti, ma credo che la combinazione più usata nei nostri brainstorming lavorativi sia proprio Canonical Data Model: “Cristiano, ci trovo questa informazione nel Canonical Data Model?”, “Posso aggiungere questo campo nel Canonical Data Model?”, “Ti invio questo oggetto del Canonical Data Model”, e così via.
Il Canonical Data Model è un pattern di integrazione Enterprise, descritto nel libro di Hohpe e Woolf, “Enterprise Integration Patterns” [1] nel quale si insegna come dominare l’arte dell’integrazione. Dopo tanto tempo, ho maturato che il significato corretto di “integrare” in ambito IT sia “scambiare informazioni tra applicazioni”:
Il libro racconta appunto come lo si possa fare grazie a pattern incentrati sul concetto di messaggi, che transitano da una applicazione a un’altra. Il Canonical Data Model in particolare fornisce un approccio da usare quando se ne debbano integrare tante tra di loro!
Come viene implementato?
Noi in pratica implementiamo il Canonical Data Model come un progetto Java, fatto di tanti POJO e interfacce così come abbiamo fatto nel modulo sensormix-datamodel-api del progetto di esempio SensorMix [2] che abbiamo raccontato nell’articolo [3] e che prenderemo a esempio di riferimento soprattutto nella seconda parte di questo articolo.
Tuttavia, nel prodotto che sviluppiamo in ambito professionale (una piattaforma di integrazione in un ambito molto specifico) il Canonical Data Model che usiamo conta ad oggi 240 classi Plain Old Java, 63 enumerazioni e 15 interfacce, in continuo aumento.
L’approccio basato su Java ci offre un’elevata flessibilità, indispensabile per rispondere alla forte dinamicità dell’applicazione su cui lavoriamo, sia in parte dovuta ad una maturità non ancora completamente raggiunta, ma che mai sarà superata definitivamente, vista la capacità di adattamento che è richiesta al nostro prodotto.
Oltre il pattern
Ma per noi il Canonical Data Model non è solo un pattern o un modulo software: rappresenta soprattutto un linguaggio e un punto di riferimento nella nostra collaborazione quotidiana tra colleghi e determina i confini dei moduli che sviluppiamo permettendoci di implementare i principi e i pattern di modularità che abbiamo descritto nell’articolo indicato in [4].
Dividiamo la tematica in due parti: la prima, che state leggendo, è più incentrata sulle considerazioni che ci hanno portato a maturare la nostra metodologia odierna, mentre invece nel secondo articolo ci concentriamo di più sui dettagli tecnici che abbiamo seguito nel nostro percorso.
L’enterprise pattern dal libro di Hohpe e Woolf
Come tutti gli Enterprise Integration Patterns (EIP), il Canonical Data Model (CDM) è proposto come soluzione a un problema specifico di integrazione. Questo problema è enunciato nel libro in questo modo:
“Come si possono minimizzare le dipendenze quando si integrano applicazioni che usano formati dati differenti?”
Applicazioni sviluppate da persone, team o aziende diverse, tendono ad usare formati dati differenti e in generale si adotta quello più conveniente alla propria applicazione a meno che non ci si sia accordati in precedenza (e a volte questa condizione non è neanche sufficiente). Anche quando il formato dati è lo stesso in termini di protocollo e linguaggio (ad esempio oggigiorno è molto comune trovare applicazioni che parlano con JSON su HTTP), la strutturazione dei dati non sarà la stessa senza regole precise come ad esempio avendo specificato uno schema.
Dal Message Translator al CDM
Negli EIP si usa il pattern Message Translator per risolvere le differenze di formato. Tuttavia applicando direttamente il pattern, qualora le applicazioni da integrare siano molteplici, il numero di “traduttori” aumenta esponenzialmente con il numero delle applicazioni integrate e presto diventa ingestibile.
Il pattern Canonical Data Model interviene proprio in questa situazione, e la sua enunciazione dice che in questi casi occorre:
“Progettare un Canonical Data Model, che sia indipendente da ogni applicazione specifica. Richiedere che ogni applicazione produca e consumi messaggi in questo formato comune”.
Il Canonical Data Model fornisce un strato di disaccoppiamento tra i singoli formati dati e, quando una nuova applicazione viene aggiunta al sistema, sarà sufficiente un singolo Message Translator per integrarla con le altre. Seguendo la notazione EIP, la soluzione si rappresenta nel seguente modo:
Il Canonical Data Model permette di tenere sotto controllo la complessità e garantisce una maggiore indipendenza tra gli elementi del sistema grazie ad uno strato di indirezione che minimizza le necessità di intervenire pervasivamente sul sistema.
I tre modi di impiego
Si possono seguire tre strade per adattare una applicazione ad un data model canonico:
- cambiare il formato dati interno alla applicazione trasformandolo nel formato canonico;
- implementare il pattern Messaging Mapper dentro l’applicazione;
- implementare un Message Translator esterno, che trasforma dal formato della applicazione al Canonical Data Model.
La prima strada non è sempre percorribile, vuoi perchè non si ha accesso all’applicazione oppure perchè modificarla in questo modo non è efficiente. La prima strada può tuttavia tornare molto pratica ed efficiente quando il sistema continua a evolversi e vengono sviluppate nuove applicazioni: nel fare ciò si trova beneficio nell’avere un modello dati di riferimento, sia per svilupparne il codice, sia per usarne la terminologia quando se ne discute. Per noi è stato così, grazie al fatto di aver adottato un approccio modulare.
La seconda strada è spesso più appropriata, sempre a patto di avere comunque accesso all’applicazione per intervenire al suo interno.
La terza, invece, è più flessibile e basta che il sistema fornisca qualche interfaccia per poterla perseguire.
Livelli a cui adottare il CDM
Vogliamo in questo articolo focalizzarci specificamente sul Java, e vedere dove applicare al meglio il Canonical Data Model. In Java, considerando il file JAR come l’elemento di riferimento per il modulo, si possono individuare i livelli di composizione [5] illustrati in figura 4.
Tipicamente gli EIP si applicano al primo livello, quello delle applicazioni. O almeno la trattazione nel libro li descrive da questo punto di vista.
Disaccoppiamento
Tuttavia minimizzare le dipendenze ovvero disaccoppiare ha una valenza più ampia. Nell’ambito delle Service Oriented Architecture (SOA) il principio si chiama loose coupling e fa parte dei fondamentali. Nelle applicazioni modulari, il disaccoppiamento tra moduli ha molta importanza, e i pattern che insegnano a sviluppare applicazioni di questo tipo [5] insegnano diverse tecniche sul modo in cui gestire le dipendenze. Ma non si disaccoppia solo nei grandi sistemi distribuiti: lo si fa, e lo si trova rimarcato come principio, anche nella semplice programmazione a oggetti dove si disaccoppia implementando la Dependecy Injection o applicando pattern come il Bridge.
Disaccoppiare può avere un costo ma, nell’ambito dello sviluppo software, l’effetto positivo si sente soprattutto sulla sua manutenibilità, di cui si abbassa la complessità al crescere del numero di righe di codice.
Abbiamo scoperto che Il Canonical Data Model permette soprattutto di disaccoppiare, e lo fa bene quando le entità da interconnettere aumentano in numero. Sia che queste entità siano delle applicazioni oppure moduli oppure classi. E funziona anche quando le entità da disaccoppiare sono membri del team di sviluppo!
CDM per moduli e classi
Da queste considerazioni, vorremmo dimostrare che il Canonical Data Model è applicabile non solo a livello di sistemi distribuiti enterprise, ma anche a livello di moduli che scambiano informazioni e che possono diventare numerosi più facilmente del numero di applicazioni. E a nostro avviso il CDM apporta benefici se applicato anche semplicemente tra classi, le quali non raramente scambiano informazioni tra di loro, ma alle spalle hanno sviluppatori differenti che le stanno implementando.
Il nostro approccio pratico ci ha permesso di sviluppare un Canonical Data Model che può essere utilizzato a tutti questi livelli.
“Progettare” un Canonical Data Model
L’enunciato del pattern usa una parola molto importante: “progettare”. Infatti è questo l’aspetto significativo nell’adottare il CDM, si deve progettare questo data model comune, e questo comporta un impatto e richiede uno sforzo.
Non è un lavoro facile: gli architetti del CDM devono sforzarsi di realizzare un data model unificato che funzioni bene in modo eguale per tutte le applicazioni che si devono integrate.
Una prima raccomandazione da seguire è focalizzarsi solo sulla porzione comune di informazioni, quelle che vengono scambiate e non quelle necessarie internamente alle singole implementazioni. Ad esempio, se sono richiesti dei metadati per l’archiviazione su database, questi non dovrebbero far parte del canonical data model: un oggetto dovrà contenere un campo id solo se quell’oggetto dovrà essere essere identificato rispetto ad altri oggetti nello scambio tra due applicazioni. Invece, il fatto che una debba memorizzare i dati su un database e quindi il record debba avere un id, non è un motivo valido per giustificare che questo campo sia riportato nel data model comune.
In secondo luogo raccomandiamo di progettare il CDM orientandolo al livello più alto, ossia al dominio di business del sistema. Non è semplice distinguere cosa sia e cosa non sia parte di questo dominio: nella nostra esperienza sono tornati utili i principi della Business Architecture così come definita nel TOGAF [6] e i principi di design dei servizi così come nelle Service Oriented Architectures [7]. La nostra adozione del CDM è stata ispirata da queste linee guida e almeno nel nostro contesto il risultato è stato efficace.
Modellare il CDM in Java con JaxB e JaxWS
Relativamente al CDM, il libro sugli EIP fa un esempio pratico: il Web Service Description Language (WSDL), ossia lo standard del W3C basato su XML Schema per descrivere le interfacce di un servizio web e che può essere usato per specificare un Canonical Data Model. Il WSDL di un servizio specifica la struttura dei messaggi delle richieste e delle risposte, e in molti casi utilizza un formato diverso da quello interno all’applicazione esponendo il sottoinsieme dei dati necessari allo scambio informazioni. Segue pertanto le raccomandazioni su come progettare un CDM.
Anche noi abbiamo cominciato a questo modo: nelle prime versioni, creavamo un WSDL e gli XML Schema ad esso associati e li importavamo nei nostri progetti usando il tool WSIMPORT di JaxWS, ottenendo in un insieme di interfacce Java e classi in stile Plain Old Java Object (POJO), legate 1 ad 1 con il WSDL grazie a JaxB.
Ostacoli dell’approccio WSDL-first
Questo approccio aveva però due grandi ostacoli che non ci rendevano soddisfatti.
Quando cambia spesso il WSDL, prima di avere disponibili le modifiche nel progetto, era necessario rigenerare le classi, perdendo diverso tempo tra comandi maven da linea di comando e refresh e clean dei progetti in Eclipse. Anche se farlo una volta ogni tanto non porta via proprio così tanto tempo, quel tempo talvolta basta a perdere la concentrazione. Se poi la modifica fatta al WSDL era un “refactoring” (magari per correggere un semplice errore di battitura di un campo che esce dalle nostre naming convention), allora diventava un vero dramma visto che i tool degli IDE non aiutano, se non segnalando prontamente la marea di errori che si manifestano dopo aver rigenerato il codice.
Il secondo ostacolo è più subdolo e ce ne siamo accorti solo dopo un po’: le classi generate non sempre sembravano simili a come le avremmo scritte noi in Java e spesso, a runtime, incorrevamo in errori di validazione dell’XML causato dall’averle utilizzate male. Questo accade perchè l’XML Schema ha costrutti molto più ricchi di quelli che si hanno nel semplice Java: progettando a partire proprio dall’XML Schema, puoi raccomandarti quanto ti pare con il team, ma si trova sempre qualcuno che ad un certo punto ti progetta un con minOccurs=”1″ e maxOccurs=”unbounded”.
In Java questo si trasforma in una java.util.List, la quale può benissimo avere zero elementi, e ci si accorge di questo solo a runtime, nel lentissimo step di validazione XML di cui non si può fare a meno in questi casi.
Se aggiungiamo il fatto che la burocrazia aziendale è stata molto più lenta dei 30 giorni di prova dell’ottimo Altova XML Spy, e che le licenze non sono arrivate in tempo, non ci abbiamo messo molto ad arrangiarci e trovare una soluzione alternativa: migrare all’approccio Java-first e cominciare dal Java.
La soluzione Java-first
Gli standard JaxB e JaxWS hanno una serie di annotazioni fatte molto bene che permettono molta espressività nel modellare classi e interfacce che poi saranno legate al WSDL. Abbiamo abbandonato WSIMPORT e siamo passati a WSGEN o, in verità, visto che adesso usiamo l’implementazione Apache CXF e non più la JaxWS-RI, usiamo l’equivalente di WSGEN che si trova in CXF, ossia JAVA2WS.
Modelliamo il nostro Canonical Data Model direttamente in Java, e per noi è molto congeniale visto che siamo appassionati sviluppatori. Esistono tra l’altro diversi plugin che creano i class diagram UML a partire dalle classi Java, fornendoci un modello visuale mentre portiamo avanti la modellazione e trasformando in scrittura di codice una noiosa parte della attività di “documentazione”. Vi raccomandiamo di provare ObjectAid [8] per Eclipse, semplice, essenziale e gratuito per la generazione dei Class Diagram. ObjectAid è capace di generare in automatico immagini UML delle classi selezionate e risponde bene ai refactoring!
Con questo approccio abbiamo superato gli ostacoli dell’approccio WSDL-first. Poichè nei moderni IDE il Java viene compilato ad ogni salvataggio, le modifiche sono immediatamente disponibili nel build path durante la modellazione; molti di quelli che erano test di integrazione sul modello generato dal WSDL sono diventati test unitari e sono rapidi da eseguire. E il refactoring del CDM non è più un problema poichè viene fatto direttamente sulla classe e propagato a tutti i progetti aperti dall’IDE. Per alcuni refactoring più ostici, è stato sufficiente importare in Eclipse tutti i progetti del sistema.
Anche il secondo ostacolo non esiste più: infatti, partendo dal Java, non c’è rischio che qualcuno utilizzi regole di validazione più strette di quanto si trova del Java, lo schema XML generato è naturalmente più vicino a come scriveremmo noi il codice, e questo vale non solo per il Java ma anche importando il WSDL così generato in altri linguaggi ad oggetti come ad esempio in C#.
Soprattutto, questo ci ha permesso di poter fare a meno completamente della lenta validazione XML che è responsabile della parte più significativa del degrado delle prestazioni quando si usano i Web Service: se un messaggio non è valido nell’XML Schema, non lo sarà neanche in Java causando direttamente delle eccezioni durante l’unmarshalling.
Trasformazione doppia contro trasformazione diretta
Adottare il CDM introduce un certo overhead nel flusso di messaggi, in quanto l’integrazione tra due applicazioni necessita di una doppia trasformazione che comporta una degradazione delle performance e un aumento della latenza, e deve essere adottato con attenzione laddove sia richiesto un alto flusso di scambio informazioni.
Un aspetto mitigante è che, con un CDM ben progettato, si riesce ad ottenere almeno una delle due trasformazioni come stateless e in questi casi l’impatto sulle prestazioni mantiene lo stesso ordine di complessità della trasformazione più complessa, vale a dire che la doppia trasformazione avrà un impatto inferiore al doppio della trasformazione diretta. Ma, soprattutto, le trasformazioni stateless possono essere facilmente parcellizzate e scalano linearmente con la potenza di calcolo disponibile.
In alternativa, in certi casi specifici dove si richiedano performance elevate, si può comunque cortocircuitare direttamente le due applicazioni senza passare dal CDM.
Il libro degli EIP raccomanda di usare la soluzione più manutenibile fintanto che requisiti di performance lo permettano, e il CDM ha come scopo principale proprio di aumentare la manutenibilità quando le applicazioni da integrare siano maggiori in senso stretto di due. A me piace inoltre ricordare le due regole di Michael Jackson sulle ottimizzazioni: non si tratta, ovviamente, della famosa pop star americana defunta, ma di un britannico chee di secondo nome fa Anthony.
- Prima regola sulla ottimizzazione dei programmi: “Don’t do it”.
- Seconda regola sulla ottimizzazione dei programmi (solo per esperti!): “Don’t do it yet”.
Conclusioni
Ci fermiamo qui per questo mese e rimandiamo al prossimo articolo per dettagli più operativi, in cui riporteremo esempi di codice per illustrare l’implementazione del Canonical Data Model e spiegarne le ragioni.