UNVERSITÀ POLITECNICA DELLE MARCHE FACOLTÀ DI INGEGNERIA Corso di Laurea Specialistica in Ingegneria Informatica Progettazione e sviluppo di un sistema software distribuito per il supporto alla vendita con “fat client” mobile multipiattaforma. Relatore: Chiar.mo Prof. Aldo Franco Dragoni Tesi di Laurea di: Emanuele Rampichini Anno Accademico 2010-­‐2011 2 1 Introduzione e obiettivi 5 2 Panoramica del progetto 2.1 Il dominio applicativo 2.1.1 Il processo di vendita tradizionale 2.1.2 Il processo di vendita automatizzato 2.1.3 La necessità della forza vendita 2.2 Il contesto 2.2.1 Lo stato dell’arte 2.2.2 Ortogonalità rispetto al sistema informatico aziendale 2.2.3 Lo scenario “Sempre connesso” 2.2.4 L’evoluzione nel tempo 2.3 Una visione d’insieme del progetto 7 7 7 10 11 12 12 12 13 13 13 3 Le tecnologie scelte 3.1 Ambiente di programmazione e linguaggio 3.1.1 L’ambiente .NET 3.1.2 Il linguaggio C# 3.2 Lo stack tecnologico nelle varie componenti 3.2.1 Integratore 3.2.2 Server 3.2.3 Client 16 16 16 22 31 31 33 34 4 L’architettura del sistema 4.1 L’integratore 4.1.1 Scambio dei dati batch 4.1.2 Database condiviso 4.1.3 Scambio di dati destrutturati in streaming 4.1.4 Chiamata a procedura remota 4.1.5 Scambio di messaggi 4.1.6 La soluzione adottata per il flusso d’importazione 4.1.7 La soluzione adottata per il flusso di esportazione 4.1.8 Accesso condiviso al database d’interscambio 4.2 Il server 4.2.1 Architettura REST 4.2.2 Utilizzo esplicito dei metodi Http 4.2.3 Stateless vs Statefull 4.2.4 Esposizione di URI con struttura a directory 4.2.5 Trasferimento dei dati in JSON o XML 4.2.6 Sicurezza 4.3 Architettura del core dell’applicazione client 4.3.1 Data Preparation 4.3.2 Data Access 4.3.3 Repository Pattern 4.3.4 Request Reponse Layer 4.3.5 Presentation layer 39 39 40 42 44 45 46 47 53 58 62 62 63 67 70 71 74 77 78 80 82 88 90 5 La qualità del software 5.1 Il Testing 5.1.1 I test unitari 5.1.2 I test unitari come strumento di design 5.1.3 I test d’integrazione 5.1.4 I test di accettazione 5.2 Metriche 93 93 94 99 101 107 110 3 5.2.1 5.2.2 5.2.3 5.2.4 Complesità Ciclomatica Profondità dell’ereditarietà Accoppiamento tra classi Indice di manutenibilità 110 111 111 112 6 Conclusioni e sviluppi futuri 114 7 Bibliografia 115 4 1 -­‐ Introduzione e obiettivi 1 Introduzione e obiettivi La presente tesi di laurea è il frutto del lavoro svolto in circa sei mesi presso le due aziende 1 coinvolte nello sviluppo di un software di classe enterprise. L'obiettivo principale della trattazione è di evidenziare e motivare in maniera sistematica le molteplici scelte tecnologiche, architetturali e metodologiche che sono state fatte in fase di progettazione e sviluppo, come risposta ai requisiti di un sistema software complesso. Nello specifico nel capitolo due sarà fatta una panoramica di alto livello del dominio in cui il software si colloca, esponendo i principali requisiti funzionali e non che hanno guidato la progettazione. Nel capitolo tre saranno discusse le scelte tecnologiche che sono state intraprese per implementare i vari componenti del sistema. Verranno analizzate in maniera approfondita le caratteristiche dell’ambiente .NET e del linguaggio C# utilizzati per l’implementazione del software. Nel quarto capitolo saranno analizzate le varie questioni architetturali che sono state affrontate durante tutto il ciclo di sviluppo in tutti gli strati del software. Particolare importanza sarà data alla parte riguardante il processo di integrazione tra sistemi informatici eterogenei e le sfide incontrate durante la progettazione. Saranno inoltre introdotti i concetti principali delle architetture a servizi di tipo REST e come tali principi sono stati applicati nello strato server del progetto. L’ultima parte del capitolo sarà dedicata all’analisi dei pattern architetturali utilizzati all’interno dell’applicazione client partendo da quelli per l’accesso ai dati, fino ad arrivare allo strato di presentazione. 1
E-­‐xtrategy S.r.l. Kilog S.r.l. 5 1 -­‐ Introduzione e obiettivi Il quarto capitolo sarà dedicato a tutto quello che riguarda la qualità del software, con una particolare attenzione alla fase di testing. Saranno inoltre analizzate diverse metriche per validare in maniera rigorosa la qualità della base di codice prodotta. La scelta di anticipare nel secondo capitolo la trattazione delle scelte tecnologiche rispetto a quelle architetturali, è derivata dalla volontà di evidenziare nella trattazione architetturale alcuni specifici aspetti ideomatici dell’implementazione che altrimenti potrebbero sembrare fuori dal contesto. 6 2 -­‐ Panoramica del progetto 2 Panoramica del progetto 2.1 Il dominio applicativo Il dominio in cui si colloca il progetto sviluppato è quello dell'automatizzazione del processo di creazione e acquisizione ordini da parte della forza di vendita di un’azienda. Gli attori che sono coinvolti nel processo di vendita nella grande distribuzione sono tre: • L'azienda fornitrice: Rappresenta l'entità che eroga servizi o crea prodotti che dovranno essere venduti. • Il cliente: Chi richiede servizi o prodotti dell'azienda fornitrice. • L'agente di commercio: Soggetto che assume in maniera stabile, senza vincolo di subordinazione, l'incarico di stabilire dei contratti commerciali tra l'azienda e dei clienti, secondo un accordo, chiamato contratto di agenzia, che lo vincola a svolgere questo mandato su una precisa area geografica. 2.1.1 Il processo di vendita tradizionale Il processo di vendita tradizionale può essere suddiviso in quattro macro fasi: Scrittura Trasmissione Interpretazione Caricamento Immagine 1: Fasi del processo di vendita tradizionale • Scrittura: La fase di scrittura è quella che avviene presso il cliente. Nel processo tradizionale l'agente di commercio, consultando un catalogo, le 7 2 -­‐ Panoramica del progetto condizioni di vendita ed eventuali promozioni, propone al cliente prodotti e servizi. Il prodotto del processo di scrittura solitamente è un ordine di vendita compilato a mano su un modello cartaceo standardizzato, secondo delle specifiche regole. • Trasmissione: Una volta compilato l'ordine, la trasmissione può avvenire tramite una postazione fissa attraverso l'uso di FAX o in alternativa per ordini importanti tramite comunicazione diretta per via telefonica. • Interpretazione: Il processo d’interpretazione avviene all'interno dell'azienda produttrice. In questa fase un addetto dell'azienda ha il compito di decifrare quanto scritto dall'agente. • Caricamento: Una volta decodificato il contenuto dell'ordine è necessaria una fase di integrazione e consolidamento dell'informazione all'interno dell'ERP Aziendale. È particolarmente semplice individuare le criticità di tale processo nei termini di efficienza ed efficacia. Input Processo Output Effettivo Immagine 2: Modello di un generico processo 𝐸𝑓𝑓𝑖𝑐𝑎𝑐𝑖𝑎 =
!"#$"# !""#$$%&'
!"#$"# !""#$%
𝐸𝑓𝑓𝑖𝑐𝑖𝑒𝑛𝑧𝑎 =
!"#$"# !""#$$%&'
!"#$%
In questo caso si può modellare come quantità di Input il costo del lavoro compiuto dagli attori coinvolti nel processo che porta un ordine dalla scrittura 8 2 -­‐ Panoramica del progetto al consolidamento nell'ERP aziendale. Il parametro in output dal processo può essere identificato con il numero di ordini correttamente presi in carico dall'ERP. Costo del lavoro compiuto da tutti gli attori coinvolti Processo che porta un'ordine dalla scrittura al consolidamento nell'ERP aziendale Ordini correttamente presi in carico dall'ERP Immagine 3: Modello del processo di consolidamento di un ordine nell'ERP La prima e più grave criticità nel processo tradizionale è quella legata all'efficacia. La condizione essenziale di un qualsiasi processo aziendale è il raggiungimento dell'obiettivo. In questo caso le fasi di scrittura, trasmissione, interpretazione e caricamento sono eseguite del tutto o in parte da persone in maniera manuale. Ciascun passaggio in cui è coinvolto un attore umano rappresenta un possibile punto di fallimento della catena che porta al raggiungimento dell'obiettivo. Situazioni comuni come interpretazione errata della calligrafia di un'agente, o errori in fase d’inserimento dati concorrono ad abbassare in maniera critica l'output effetivo del processo e quindi la sua efficacia. D'altro canto è anche semplice capire come l'efficienza sia molto migliorabile. Nel processo tradizionale appena presentato, anche ragionando al netto degli errori di cui abbiamo parlato, il consolidamento di un ordine richiede tempi nell'ordine dei minuti. 9 2 -­‐ Panoramica del progetto 2.1.2 Il processo di vendita automatizzato Le problematiche esposte nel precedente paragrafo possono essere risolte con l'automatizzazione del processo. Nello specifico si può analizzare come le varie fasi del processo di vendita tradizionale possono essere soddisfatte da un sistema informatico. • Scrittura: La fase di scrittura è quella che avviene presso il cliente. Invece di consultare catalogo e condizioni di vendita su carta l'agente può cercare direttamente dal suo sistema informatico portatile, inserendo in maniera contestuale alla ricerca, prodotti o servizi già codificati all’interno di un ordine. • Trasmissione: L'ordine è validato in fase di creazione e compilazione. L'ordine è spedito immediatamente attraverso internet. L'unico requisito per la spedizione dell'ordine è la presenza di connessione dati di qualsiasi tipo (wifi, 3g, gprs). • Interpretazione: La risorsa ordine inviata dal tablet è persistita da un server collocato all'interno dell'azienda produttrice. Successive validazioni possono essere fatte lato server in maniera automatizzata prima della persistenza. • Caricamento: Il caricamento vero e proprio nell'ERP aziendale rappresenta l'ultimo passo. Un processo automatizzato trasforma l'ordine per adattarlo ad una struttura di interscambio precedentemente concordata. 10 2 -­‐ Panoramica del progetto Riprendendo le considerazioni su efficienza ed efficacia esposte in precedenza si può vedere come il processo automatizzato minimizza tutte le crititicità esposte. Nello specifico il processo di scrittura è molto più rapido, poichè assistito dall'elaboratore. Inoltre il fatto che l'agente interagisca direttamente con i dati delle anagrafiche correttamente codificati fa si che non ci siano errori in fase di stesura dell'ordine. La criticità della trasmissione è solamente legata allo stato della connessione di rete ed è completata nell'arco di tempo di alcuni secondi. Gli errori dovuti al processo d’interpretazione in un sistema a regime sono da considerarsi in sostanza assenti poichè i dati sono inviati in formati interpretabili in maniera corretta dalla macchina. Sia in fase di stesura dell'ordine che in fase d’interpretazione possono, a loro volta, essere eseguite delle regole di business personalizzate per la validazione dell'ordine. Rientrano in questa categoria di operazoni condizioni particolari per clienti o per tipologie merceologiche, blocco automatico di acquisti per clienti insolventi e altre regole simili. È particolarmente vantaggiosa la possibilità di applicare questo tipo di regole di business direttamente in fase di creazione dell'ordine presso il cliente, cosa in pratica impossibile in caso di processo di vendita tradizionale. Nel caso tradizionale, una condizione non valida per un determinato cliente, può essere notificata all'agente solo dopo un lungo periodo, con tutte le conseguenze del caso. 2.1.3 La necessità della forza vendita Proseguendo lungo la linea di ragionamento dell’ottimizzazione del processo si potrebbe arrivare a semplificare ulteriormente la catena eliminando completamente la figura dell’agente di vendita. 11 2 -­‐ Panoramica del progetto Dal punto di vista tecnico in un sistema di vendita informatizzato come quello descritto nel precedente paragrafo, la presenza dell’agente di vendita funge da mera interfaccia tra il cliente e il sistema informativo aziendale. Sebbene in alcuni ambiti la rimozione della figura intermedia dell’agente commerciale si sia rivelata una scelta valida2, in altri contesti, soprattutto nei rapporti business to business, continua a rappresentare un valore aggiunto per spingere le vendite. Il rapporto umano rappresenta ancora un parametro importante per la fidelizzazione del cliente. 2.2 Il contesto Il dominio non è sufficiente per capire a pieno le necessità che copre un software di questo tipo. Per comprendere al meglio le esigenze è necessario fare alcune considerazioni dell‘ambiente in cui andrà a collocarsi. 2.2.1 Lo stato dell’arte Strumenti come quelli descritti esistono da svariati anni come soluzioni verticali profondamente integrate con l’ERP. Prodotti SAP, Oracle forniscono in parte alcune funzionalità di questo tipo ma moltissime imprese sono legate all’utilizzo di gestionali legacy nati in periodi di tempo in cui lo scenario di mobilità totale non poteva essere al centro delle scelte dei progettisti. 2.2.2 Ortogonalità rispetto al sistema informatico aziendale Il software che è stato progettato in questo contesto deve potersi integrare in maniera semplice con sistemi legacy, senza particolari prerequisiti. L’ortogonalità della soluzione software rispetto al sistema informatico aziendale è uno dei valori fondanti che ha guidato la progettazione e lo sviluppo dell’applicazione. 2 Amazon, ed altri portali di commercio online hanno basato su questa strategia la propria crescita esponenziale negli ultimi anni. 12 2 -­‐ Panoramica del progetto 2.2.3 Lo scenario “Sempre connesso” Sebbene si possa dire che l’evoluzione delle tecnologie stia trasformando il mondo del lavoro in uno scenario “sempre connesso”, l’esigenza reale di un agente commerciale è quella di portare a termine il proprio lavoro anche in assenza di connessione di rete. Questo significa che dal punto di vista della creazione di un prodotto bisogna pensare un sistema che sia capace da subito di lavorare sia in modalità offline che online. La scelta di creare un sistema del genere porta delle sfide progettuali non indifferenti ma allo stesso tempo garantisce un valore immediato che pochi altri competitor sono in grado di garantire. 2.2.4 L’evoluzione nel tempo L’altro principio cardine che ha condizionato in molti aspetti la progettazione del software è legato alla volontà di gettare le basi per un progetto da far evolvere nel tempo, capace di reagire ai repentini cambi di scenario. Lo sviluppo di codice il più possibile portabile tra le maggiori piattaforme mobile è stato individuato come uno dei punti centrali su cui concentrarsi, soprattutto per l’estrema vivacità del mercato dei sistemi operativi mobile che in questo momonto storico può essere registrata. Allo stesso tempo sono state fatte scelte per non compromettere la possibilità di sviluppare client non legati a nessun sistema operativo in particolare, facendo uso di tecnologie e standard WEB. 2.3 Una visione d’insieme del progetto Il progetto nella sua visione di alto livello può essere rappresentato dalla seguente figura. 13 2 -­‐ Panoramica del progetto Immagine 4: Schema concettuale di alto livello del prodotto sviluppato Nell’immagine possiamo vedere in maniera generale gli attori coinvolti. Alla sinistra abbiamo molteplici software gestionali che sono coinvolti in una comunicazione bidirezionale con il sistema sviluppato. Il progetto che è stato portato avanti può essere suddiviso in tre macro aree con precise responsabilità: • L’integratore Si occupa della gestione dei flussi di import ed export di dati con il gestionale aziendale. Rappresenta l’unico punto di contatto con l’ERP dell’azienda. • Server Espone tutti i servizi di sincronizzazione, autenticazione, configurazione e comunicazione consumabili da un qualsiasi client. • Fat Client Usufruisce dei servizi di sincronizzazione e di comunicazione con il server. Una volta sincronizzato ha al suo interno tutta la logica di 14 2 -­‐ Panoramica del progetto business e la porzione di dati necessaria a svolgere la maggior parte dei compiti richiesti dall’agente di commercio. Sono esclusi I compiti che implicano una comunicazione con il server. 15 3 -­‐ Le tecnologie scelte 3 Le tecnologie scelte Come già spiegato nell’introduzione, in questo capitolo si è deciso di presentare le tecnologie scelte per l’implementazione dei diversi progetti sviluppati. Il capitolo non vuole in alcun modo essere una trattazione esaustiva dell’infrastruttura tecnologica, quanto piuttosto una necessaria introduzione per mettere nel giusto contesto quanto spiegato nei capitoli successivi. Le scelte compiute saranno comunque motivate in relazione agli obbiettivi del progetto. 3.1 Ambiente di programmazione e linguaggio 3.1.1 L’ambiente .NET L’ambiente .NET è un framework generalmente associato al sistema operativo Microsoft Windows. Il progetto include una vastissima libreria standard per tutte le esigenze comuni di applicazioni Enterprise. Il framework .NET grazie all’utilizzo di una virtual machine che consuma Bytecode intermedio supporta una grande varietà di linguaggi di programmazione. I programmi scritti per il .NET framework vengono eseguiti in un’ambiente software conosciuto come CLR3, una macchina virtuale che fornisce servizi fondamentali. Fanno parte di questi servizi la sicurezza, il memory management e il sistema di gestione delle eccezioni. 3
Common Language Runtime 16 3 -­‐ Le tecnologie scelte Class Library CLR .NET Framework Immagine 5: Composizione del .NET framework Il .NET framework non è nient’altro che l’insieme formato dal CLR ed un’estesa class library che gira sullo stesso. Immagine 6: Stack delle API del .NET framework Le funzionalità di alto livello fornite dal framework comprendono: • Gestione dell’accesso ai dati • Connessione con basi di dati • Comunicazione di rete • Crittografia • Algoritmi di calcolo numerico 17 3 -­‐ Le tecnologie scelte • Creazione di interfaccie utente I motivi principali che hanno portato a questa scelta riguardano l’estrema maturità dello stack tecnologico, e la bontà degli strumenti di sviluppo che ruotano intorno a questà tecnologia. I principi fondanti del framework .NET possono essere riassunti in sette punti e per ciascun punto può essere indicato il valore portato in maniera diretta al progetto: • Interoperabilità: Il framework permette di interfacciarsi in maniera semplice a programmi che sono stati sviluppati con altre tecnologie legacy scollegate dall’ambiente .NET. Esistono packages per l’interfacciamento con moduli scritti per il modello COM 4 ed ad un livello più basso funzionalità di Platform Invoke5 per chiamare una qualsiasi funzione di una qualsiasi libreria compilata nativamente non scritta per il .NET framework. Queste caratteristiche sono particolarmente interessanti nella misura in cui nell’ambito dell sviluppo enterprise può presentarsi l’esigenza di integrare all’interno del progetto codice di librerie largamente collaudate legate a tecnologie eterogenee. • Indipendenza Dal Linguaggio: Il fatto che il framework poggi sul Common Language Runtime, significa che tutto il codice scritto in un programma .NET viene eseguito sotto la sua supervisione garantendo comportamenti precisi e prevedibili per quanto riguarda la gestione della memoria, la sicurezza e la gestione 4
Component Object Model 5
PInvoke 18 3 -­‐ Le tecnologie scelte delle eccezioni. La presenza del CLR garantisce inoltre l’indipendenza dal linguaggio di programmazione scelto. Il CLR esegue un particolare tipo di codice intermedio chiamato CIL6 (linguaggio assembly di alto livello). Qualsiasi linguaggio che abbia un compilatore per il codice intermedio CIL può essere utilizzato in ambiente .NET. Immagine 7: Schema di funzionamento della CLI Anche questa caratteristica nel caso del progetto svolto si è rivelata particolarmente utile. La possibilità di integrare in maniera trasparente class library scritte all’interno dell’azienda per altri progetti in linguaggi 6
Common Intermediate Language 19 3 -­‐ Le tecnologie scelte diversi ma sempre su framework .NET ha permesso di annullare per determinati componenti non direttamente legati al dominio (gestione dei log, gestione dello scheduling di operazioni lunghe) i tempi di sviluppo. L’altro vantaggio non immediatamente riscontrato è quello della tranquillità con cui si possono valutare le nuove proposte tecnologiche. Il mondo dei linguaggi di programmazione ultimamente è in continuo fermento e vecchi paradigmi come quello funzionale stanno uscendo dall’ambito prettamente accademico per affacciarsi a quello del software enterprise. La prospettiva di poter fare un salto di paradigma, mantenendo comunque la vecchia base di codice funzionante e integrabile in maniera trasparente rende un’eventuale strategia di migrazione futura molto più morbida e graduale, con tutti I vantaggi del caso. • Estesa Base Class Library: Il .NET framework fornisce una class library di base disponibile a tutti i linguaggi scritti per girare all’interno del CLR. Le funzionalità fornite spaziano in tutti gli ambiti come gestione dei file, l’interazione con database, supporto all’implementazione di protocolli di rete, creazione di interfacce grafiche e molto altro. Le applicazioni enterprise complesse spaziano tra esigenze molto eterogenee. Nel progetto specifico la class library è stata utilizzata in maniera estensiva. Poter far affidamento su librerie di sistema con elevati standard qualitativi per assolvere i compiti infrastrutturali permette di concentrare le proprie forze sulla risoluzione dei problemi del dominio di business. 20 3 -­‐ Le tecnologie scelte • Supporto in fase di deployment Il framework fornisce strumenti per la gestione del deployment dei progetti. La configurazione della build e quella centralizzata delle impostazioni di ambiente con possibilità di regole particolari per le macchine dei singoli sviluppatori, sono solo alcune delle caratteristiche fornite. A questo livello possono essere anche definite le policy di sicurezza del prodotto da deployare in maniera dinamica. Il vantaggio principale che questa caratteristica ha portato è stato quello di poter configurare in maniera agevole le varie installazioni, differenziando le configurazioni per gli ambienti di lavoro degli sviluppatori da quelle per il deploy presso specifici clienti. • Sicurezza: L’intero framework è stato progettato e sviluppato con la sicurezza in cima alla lista degli obiettivi. Il framework mette al riparo il proprio codice dalle classiche vulnerabilità del codice nativo come l’utilizzo di Buffer Overflow per scalare i privilegi su una macchina di produzione. Sarebbe possibile motivare l’estrema bontà del modello di sicurezza su cui è stato costruito il CLR, ma la cosa esulerebbe dagli scopi della trattazione. Per chi fosse interessato, è comunque disponibile un interessante paper, pubblicato dal dipartimento di informatica dell’Università Della Virginia7, che lo mette a confronto con quello della JVM8. 7
Comparing Java and .NET Security: Lessons Learned and Missed: Nathanael Paul, David Evans University of Virginia Department of Computer Science 8
Java Virtual Machine 21 3 -­‐ Le tecnologie scelte In questo caso il vantaggio è implicito. Poter fare affidamento su uno stack che garantisca livelli di sicurezza allo stato dell’arte, è senza dubbio un enorme valore per applicazioni business. • Portabilità Nonostante la Microsoft non abbia mai realizzato direttamente un’implementazione del framework completo all’infuori del proprio sistema operativo Windows, il framework è stato progettato per essere agnostico rispetto alla piattaforma. Le specifiche della CLI 9 che includono le librerie core, il type system, il CIL, il linguaggio C#, il linguaggio C++/CLI managed, sono state rilasciate all’ECMA e all’ISO e sono disponibili come open standard10. Questa caratteristica è stata senza dubbio quella più importante per la scelta della tecnologia. La disponibilità di specifiche aperte ha permesso l’implementazione dell’alternativa Open Source MONO, che ha portato un’implementazione di primo livello del framework .NET su tutti I sistemi operativi principali. 3.1.2 Il linguaggio C# Il linguaggio utilizzato per l’implementazione vera e propria delle varie componenti del software sviluppato è il C#. Come visto nel paragrafo precedente C# oltre ad essere uno dei tanti linguaggi supportati dal CLR fa parte della CLI come parte dello standard ISO/IEC 23271 e ECMA 335. Le principali caratteristiche del linguaggio possono essere comprese dai suoi principi fndanti: 9
Common Language Infrastructure 10
ISO/IEC 23271 e ECMA 335 22 3 -­‐ Le tecnologie scelte •
C# è stato progettato da un team guidato da Anders Hejlsberg11 con l’obbiettivo di creare un linguaggio semplice, moderno, general purpose ed object oriented.
•
Il linguaggio appartiene alla famiglia dei linguaggi statici a tipizzazione forte, anche se elementi di dinamismo sono stati inseriti in seguito nel linguaggio.
•
Il linguaggio presenta inoltre meccanismi di garbage collection per la gestione automatica della memoria.
Analizzando solo queste caratteristiche principali, molti potrebbero rivedere un parallelo molto forte tra il mondo Java e questo. In fondo si tratta di linguaggi statici, fortemente tipizzati, object oriented che compilano in codice intermedio per una macchina virtuale. Il paragone è naturale e giustificato ma il linguaggio C# ha saputo nel tempo innovarsi e accogliere tutta una serie di caratteristiche avanzate che lo hanno di fatto reso un linguaggio fortemente caratterizzato. In questa fase ne vedremo alcune particolarmente interessanti che sono state introdotte nelle varie versioni dello standard. • Generics: Grazie ai generics è possibile progettare classi che rinviano la conoscenza del tipo fino a quando il codice client non crea un’istanza della classe specificando il tipo come parametro. 11 Come nota storica è interessante notare che Anders Hejlsberg prima di intraprendere il suo lavoro su C# era già famoso nell’ambiente dei linguaggi di programmazione per aver contribuito in maniera fondamentale alla creazione del più celebre dialetto del Pascal (Turbo Pascal della Borland) e del suo diretto derivato ad oggetti Delphi (della medesima Software House Francese) 23 3 -­‐ Le tecnologie scelte // Dichiarazione di una classe generica
public class GenericList<T>
{
void Add(T input) { }
}
class TestGenericList
{
private class ExampleClass { }
static void Main()
{
// Dichiara una lista di tipo int
GenericList<int> list1 = new GenericList<int>();
// Dichiara una lista di tipo stringa
GenericList<string> list2 = new GenericList<string>();
// Dichiara una lista di tipo ExampleClass
GenericList<ExampleClass> list3 =
new GenericList<ExampleClass>();
}
}
Snippet 1: Generics in C# Questa caratteristica è stata utilizzata spesso ed ha permesso di mantenere un alto riuso di codice in diverse situazioni. • Supporto ai tipi parziali Permettono di separare l’implementazione di una struct, una classe o un’interfaccia in più file sorgenti. In fase di compilazione I tipi parziali vengono riassemblati. 1. // Prima parte della classe parziale customer
2. public partial class Customer
3. {
4.
public void DoSomething()
5.
{
6.
}
7. }
8.
9. // Seconda parte della classe parziale
10. // Può essere messa in un altro file sorgente
11. public partial class Customer
12. {
13.
public void DoSomethingElse()
14.
{
15.
}
16. }
Snippet 2: Partial types in C# 24 3 -­‐ Le tecnologie scelte Sebbene la feature non sia particolarmente straordinaria, è stata utile per le alcune classi in cui una parte è generata automaticamente. • Variabili locali tipizzate in modo implicito In fase di dichiarazione di variabili locali è possibile utilizzare la parola “chiave” var invece di scrivere esplicitamente il tipo. Sarà dedotto automaticamente dal compilatore in base all’espressione alla destra dell’operatore di assegnamento. 1. // i viene compilato come intero
2. var i = 5;
3.
4.
5.
6. // s viene compilato come stringa
7. var s = "Hello";
8.
9.
10. // a viene compilato come array di stringhe
11. var a = new[] { 0, 1, 2 };
12.
13.
14. // list viene compilato come List<int>
15. var list = new List<int>();
Snippet 3: Tipizzazione implicita in C# Questa caratteristica è una di quelle che aumenta di molto la leggibilità del codice rendendolo più conciso. • First Class Functions (Delegates) Il C# possiede dalla versione 2.0 il concetto di funzione come tipo di dato nella forma dei delegates. I delegates sono oggetti che referenziano un determinato comportamento. I delegates permettono tecniche di programmazione avanzate come l’high order programming: è possibile costruire funzioni che prendano come parametro uno o più delegate e che addirittura restituiscano delegate come valore di ritorno. 25 3 -­‐ Le tecnologie scelte 1. class Program
2. {
3.
// Dichiarazione di un delegate
4.
delegate string UppercaseDelegate(string input);
5.
6.
// Prima Implementazione
7.
static string UppercaseFirst(string input)
8.
{
9.
char[] buffer = input.ToCharArray();
10.
buffer[0] = char.ToUpper(buffer[0]);
11.
return new string(buffer);
12.
}
13.
14.
// Seconda implementazione
15.
static string UppercaseLast(string input)
16.
{
17.
char[] buffer =
18.
input.ToCharArray();
19.
buffer[buffer.Length - 1] =
20.
char.ToUpper(buffer[buffer.Length - 1]);
21.
return new string(buffer);
22.
}
23.
24.
// Terza implementazione
25.
static string UppercaseAll(string input)
26.
{
27.
return input.ToUpper();
28.
}
29.
30.
// High order function che riceve un delegate
31.
// come parametro
32.
static void WriteOutput(string input, UppercaseDelegate del)
33.
{
34.
Console.WriteLine("Input: {0}", input);
35.
Console.WriteLine("Uppercase: {0}", del(input));
36.
}
37.
38.
39.
// Implementazione dello strategy pattern con delegates
40.
static void Main()
41.
{
42.
// Input: pippo Uppercase: Pippo
43.
WriteOutput("pippo", new UppercaseDelegate(UppercaseFirst));
44.
// Input: pippo Uppercase: pippO
45.
WriteOutput("pippo", new UppercaseDelegate(UppercaseLast));
46.
// Input: pippo Uppercase: PIPPO
47.
WriteOutput("pippo", new UppercaseDelegate(UppercaseAll));
48.
}
49. }
Snippet 4: Implementazione di strategy con delegates in C# L’high order programming è un altro strumento molto utile allo scopo di ridurre al minimo la duplicazione di codice, e come direttamente riscontrabile nell’esempio rende molto meno verbosa l’introduzione di alcuni design pattern che avrebbero richiesto l’uso di interfacce. 26 3 -­‐ Le tecnologie scelte • Espressioni Lambda Un’espressione lambda è una funzione anonima che può contenere al suo interno espressioni ed istruzioni e che può essere assegnata a variabili di tipo delegate. Le espressioni lambda utilizzano uno speciale operatore12 che può essere letto come “fino a”. 1. delegate int Del(int i);
2.
3. static void Main(string[] args)
4. {
5.
// Istanzianzione di un delegate
6.
// con assegnazione di una
7.
// espressione lambda
8.
Del myDelegate = x => x * x;
9.
int j = myDelegate(5); //j = 25
10. }
Snippet 5: Lambda expressions in C# L’utilizzo delle espressioni lambda è una delle caratteristiche dei linguaggi funzionali che sono state introdotte nel linguaggio C#. • LINQ13 Tra le peculiarità prese in prestito dai linguaggi “funzionali” del C# non può non essere citato LINQ che rappresenta un vero e proprio linguaggio dichiarativo di query su collezioni di qualsiasi tipo. Le query possono essere fatte con una sintassi simile a un linguaggio della famiglia SQL o con un’interfaccia fluente più vicina al modello della programmazione classica. 12
=> rappresenta l’operatore lambda in C# 13
Language Integrated Query 27 3 -­‐ Le tecnologie scelte 1. class Program
2. {
3.
static void Main()
4.
{
5.
// Query con sintassi fluente
6.
var title = entries
7.
.Where(e => e.Approved)
8.
.OrderBy(e => e.Rating)
9.
.Select(e => e.Title)
10.
.FirstOrDefault();
11.
12.
// Query con sintassi SQL-Like
13.
var query = (from e in entries
14.
where e.Approved
15.
orderby e.Rating
16.
select e.Title).FirstOrDefault();
17.
}
18. }
Snippet 6: Utilizzo di LINQ per query su oggetti in memoria Le caratteristiche offerte sono molteplici ed alcune di esse possono essere mappate direttamente con i nomi più conosciuti in ambito funzionale: LINQ Functional Programming Select Map Where Filter SelectMany Bind Sum/Max/Min/Avg Fold + Funzione Appropriata Aggregate Fold Dalla rapida carrellata di alcune delle caratteristiche peculiari del linguaggio si può notare come siano entrate caratteristiche anche molto avanzate, facendo divergere sensibilmente il linguaggio da quello che, nelle prime versioni, era additato come un maldestro clone del Java. Il linguaggio è moderno e molto espressivo, ed è continuamente migliorato. La massiccia adozione e la maturità di linguaggio e ambiente dimostrano la bontà della scelta. Nella classifica della popolarità dei linguaggi di 28 3 -­‐ Le tecnologie scelte programmazione stilata da Tiobe 14 aggiornata a gennaio 2012 possiamo vedere un balzo nell’interesse generato dal linguaggio: Table 1: Classifica stilata da TIOBE dei linguaggi più popolari aggiornata a gennaio 2011 Sempre TIOBE fornisce un grafico dell’interesse generato dalla tecnologia su un periodo di dieci anni: 14
I dati del TIOBE index rappresentano un aggregato dell’attività online su determinate chiavi di ricerca. Non rappresentano in alcun modo un dato rigoroso sull’adozione del linguaggio quanto piuttosto un indicatore dell’interesse che c’è intorno ad una tecnologia. 29 3 -­‐ Le tecnologie scelte Immagine 8: Grafico dell'andamento della popolarità dei linguaggi di programmazione su un periodo di 10 anni Com’è facile vedere il linguaggio C# presenta un andamento di generale e costante crescita. 30 3 -­‐ Le tecnologie scelte 3.2 Lo stack tecnologico nelle varie componenti Nel paragrafo precedente abbiamo inquadrato l’ambiente di programmazione e il linguaggio utilizzati per il progetto in generale. In questo paragrafo saranno analizzate in maniera più specifica le tecnologie utilizzate nelle varie componenti del sistema. Le varie soluzioni saranno introdotte nell’ambito in cui sono state utilizzate, seguendo come traccia le macro aree di responsabilità individuate nel primo capitolo. 3.2.1 Integratore Le principali responsabilità dell’integratore consistono nella gestione dei flussi di import ed export di dati con il gestionale aziendale. In generale è il punto di comunicazione tra il database aziendale e quello del server della soluzione sviluppata. Integrator Immagine 9: Processo di integrazione da un database di interscambio e SQL Server Il processo che svolge l’integratore in fase di import coinvolge diverse tecnologie. Lato ERP non c’è garanzia per quanto riguarda il DBMS utilizzato. In questo momento le soluzioni supportate sono tutte quelle che sono in grado di esporre un’interfaccia di tipo ODBC15. 15
Open DataBase Connectivity 31 3 -­‐ Le tecnologie scelte Immagine 10: Astrazione delle sorgenti dati con driver e interfaccia ODBC Utilizzando questo layer di astrazione si possono coprire quasi completamente le più disparate esigenze d’integrazione. Sono disponibili driver ODBC per una varietà estrema16 di sorgenti dati che spazia tra I DBMS di classe enterprise più famosi fino a driver per l’interfacciamento con filesystem o sorgenti dati di tipo documentali. La scelta di partire con una soluzione di questo tipo ha permesso di avere una grande quantità di sistemi supportati sin da subito, ma le possibilità non sono limitate a questo tipo d’interfacciamento. Vedremo in maniera più approfondita nel prossimo capitolo quale strategia d’integrazione è stata adottata e come questa permetta di adattarsi a scenari in cui fosse necessario integrare dei dati provenienti dalle fonti più disparate17. 16
Una lista completa dei driver forniti da diversi vendor può essere consultata al seguente indirizzo: http://web.synametrics.com/odbcdrivervendors.htm 17
Una potenziale richiesta potrebbe essere quella di integrare dati provenienti da servizi web aziendali. 32 3 -­‐ Le tecnologie scelte 3.2.2 Server La soluzione server è stata sviluppata su tecnologia Microsoft, avendo come sistema operativo target Windows Server 2003 e successivi. Per quanto riguarda il DBMS relazionale è stato scelto Microsoft SQL Server. Sebbene l’intero progetto sia stato sviluppato su queste fondamenta tecnologiche, si è cercato di isolare la scelta della tecnologia del database in modo da poter cambiare in futuro DBMS in maniera indolore. In questo senso tutte le query di business logic che sono fatte non fanno uso di particolari feature avanzate del DBMS. Si è deciso di lasciare da parte caratteristiche avanzate come stored procedures, servizi di replicazione, di message broking e l’exception handling integrato nelle query SQL. Gli svantaggi di una scelta del genere sono legati al fatto che l’utilizzo di tali caratteristiche avrebbe semplificato alcuni processi che sono stati reimplementati, ma presenta allo stesso tempo due grandi vantaggi da non sottovalutare, uno tecnico ed uno strategico: • Possibilità di condividere la logica di accesso ai dati con il client: Come abbiamo visto la soluzione sviluppata include un fat client mobile. Facendo uso esclusivamente di caratteristiche standard dell’SQL si è riuscito a fare in modo che tutte le query scritte per l’accesso ai dati possano girare in maniera identica su ipad con DBMS sqlite e sul server con DBMS Sql Server. Questo vantaggio tecnico non è assolutamente da sottovalutare nell’ottica in cui permette di spostare con estrema facilità pezzi di logica dal client al server e viceversa. • Protezione dal vendor Lock-­‐in: La scelta di abbracciare in pieno una tecnologia non standard può portare con sé problemi a livello di business in una visione a lungo 33 3 -­‐ Le tecnologie scelte termine. Legare buona parte del software a una soluzione commerciale senza pensare ai costi di un’eventuale migrazione è una pratica che andrebbe sempre evitata, soprattutto nel caso di software di classe Enterprise con ciclo di vita molto lungo. 3.2.3 Client Il titolo di questa tesi elenca due caratteristiche per quanto riguarda il client: • “FAT18” • Multipiattaforma Se consideriamo una classificazione architetturale dal punto di vista strutturale, ci troviamo in una architettura a due strati. Immagine 11: Differenti architetture client server in base alla suddivisione delle responsabilità Nella fattispecie, il diagramma evidenzia come un’architettura a due strati possa essere suddivisa in conformità a come sono distribuite le responsabilità 18
Per “fat client” s’intende pesante, in relazione al fatto che gestisce direttamente dati, business logic e presentazione, in contrapposizione al “thin client” leggero che si occupa solo della presentazione. 34 3 -­‐ Le tecnologie scelte tra client e server. Alla sinistra troviamo le soluzioni di tipo thin client, mentre spostandoci verso destra, raggiungiamo quelle “fat”. La soluzione progettata può essere inquadrata perfettamente nella categoria “Gestione distribuita dell’applicazione e dei dati”. Sebbene la scelta compiuta nella prima fase del progetto sia stata quella di fornire soltanto un client per piattaforma iPad, nella visione di lungo termine le maggiori piattaforme dovranno avere un loro client dedicato. In uno scenario ancora più a lungo termine, quando sarà possibile a livello d’infrastruttura nazionale pensare a uno scenario in cui gli agenti possano lavorare sempre connessi, l’idea è di fornire client basati su tecnologia standard WEB. Analizzando lo scenario attuale possiamo vedere come le tre piattaforme “mobile” principali che sono sul mercato adottino stack tecnologici completamente diversi: • Apple iOS Al centro della piattaforma tecnologica apple ci sono il linguaggio objective-­‐C e il framework cocoa touch. In alternativa per scrivere codice di libreria che non fa Immagine 12: Apple Logo direttamente uso delle peculiarità della piattaforma, è possibile utilizzare I classici linguaggi che compilano in codice nativo come C e C++. • Google Android La tecnologia nativa della piattaforma android è una reimplementazione opensource della Java Virtual Immagine 13: Android Logo 35 3 -­‐ Le tecnologie scelte Machine19 . A differenza del caso apple con un minimo di sforzo si potrebbe scrivere codice che può essere compilato dalla JVM standard. Anche su android è possibile utilizzare codice nativo attraverso l’uso di chiamate JNI20 • Windows Mobile 7 / Windows 8 La piattaforma Windows Mobile 7 permette solo ed esclusivamente l’utilizzo di codice managed scritto in linguaggio C# su runtime .NET. Allo stato attuale non è Immagine 14: Windows Mobile Logo possibile far girare codice nativo compilato. Windows 8 girerà su tablet e sarà un sistema general purpose molto più vicino ai S.O. desktop attuali, e supporterà in piento l’ambiente .NET. La possibilità di precludersi uno degli ambienti è sicuramente da scongiurare, soprattutto in un mercato instabile come quello mobile. Rischiare di relegare il proprio prodotto all’obsolescenza poichè si è puntato sul cavallo sbagliato non è sicuramente una soluzione percorribile. Per capire le conseguenze di come la scelta di una tecnologia si ripercuote sull’utilizzo delle altre possiamo vedere la seguente tabella degli scenari: Linguaggio Business Scelto Business logic Business logic su Busines logic logic su iPad su android Windows Mobile nel Server ObjectiveC Si No No No Java Si No Si21 No 19
Il progetto open source alla base del sistema operativo android si chiama Dalvik Virtual Machine. 20
Java Native Interface. 21
Legato al fatto di riconsiderare la tecnologia scelta per l’ambiente server o all’utilizzo di tecniche d’integrazione molto complesse. 36 3 -­‐ Le tecnologie scelte C/C++ Si Si No Si C#/.NET Si* Si* Si Si Tabella 1: Legame tra scelta del linguaggio e scenari possibili Leggendo la tabella si può notare come sia stata inserita nella riga dedicata a C#/.NET la possibilità di trasportare la business logic sia su iOs che Android. Sebbene questa possibilità non sia offerta in maniera nativa dai due sistemi operativi, la cosa è possibile grazie a tre progetti portati avanti dall’azienda Xamarin22: • Mono Come evidenziato in precedenza C# e .NET fanno parte della CLI standard ECMA/ISO. Tale standardizzazione ha permesso la creazione di un’implementazione alternativa Open Source c
Immagine 15: Mono Logo o
nosciuta con il nome MONO. Tale implementazione è estremamente matura ed è stata portata su una amplissima varietà di sitemi operativi (BSD, Linux, Windows, Solaris, OSX, Android, iOS, Playstation 3, Xbox360, Nintendo Wii). • MonoTouch MonoTouch permette di sviluppare per iOS utilizzando il linguaggio C# e il framework .NET. Il prodotto commerciale basato su Mono offre tutte le API di iOS altrimenti disponibili solo utilizzando linguaggio nativo Objective-­‐C. Una particolarità del compilatore MonoTouch è la capacità di produrre codice nativo e non codice intermedio compilato 22
Mono, MonoTouch, MonoForAndroid sono stati inizialmente creati da Ximian, successivamente acquistata da Novell e passati poi a Xamarin con il passaggio della proprietà di Novell ad Attachmate. 37 3 -­‐ Le tecnologie scelte JIT23. Questa scelta è stata dettata principalmente dalle politiche apple che vietano l’utilizzo di compilatori JIT sui propri dispositivi. • MonoForAndroid MonoForAndroid è la controparte android di MonoTouch. Com’è facile immaginare la scelta è ricaduta sull’utilizzo del linguaggio C# e del framework .NET. Poter utilizzare la stessa tecnologia attraverso tutti I layer dell’architettura in un sistema complesso porta moltissimi vantaggi che vanno oltre a quelli prettamente tecnici. Da una parte si ha la possibilità di condividere codice, test, tool e librerie, dall’altra permette di evitare cambi di contesto per gli sviluppatori. 23
Just in Time 38 4 -­‐ L’architettura del sistema 4 L’architettura del sistema In questo capitolo si analizzeranno le scelte che sono state fatte per quanto riguarda le aree di responsabilità del software a un livello non prettamente tecnologico ma architetturale. La trattazione delle singole problematiche seguirà il seguente approccio: sarà inizialmente presentato lo scenario generico del problema e le possibili soluzioni che si possono trovare in letteratura. In seguito sarà spiegata la soluzione vera e propria applicata al problema reale. 4.1 L’integratore L’integrazione in ambito Enterprise è un tema delicato. La necessità di integrazione è un processo altamente prioritario per quasi tutte le aziende. La possibilità di offrire nuovi servizi esponendo parti del proprio bagaglio tecnologico legacy è pratica sempre più comune. Sono disponibili sul metcato diverse tecnologie e strumenti a supporto del complesso compito di integrare sistemi informatici eterogenei. Strumenti di questo tipo (che d’ora in poi chiameremo EAI24 per brevità) offrono spesso meccanismi di messaggistica proprietari, completati da potenti tool per la gestione dei metadati e per l’editing grafico dei processi di trasformazione dei dati. Tali strumenti forniscono spesso componenti già pronti per l’integrazione con le più popolari applicazioni business. Molti dei produttori di tool in questione spingono spesso verso soluzioni e su best practice legate alla specifica tecnologia fornita. Nella realtà la creazione di una architettura di integrazione in ambito enterprise è una problematica che non può essere facilmente standardizzato e richiede spesso un’approccio 24
Enterprise Application Integration 39 4 -­‐ L’architettura del sistema critico rispetto alle esigenze specifiche. Senza indugiare oltre sui motivi che portano un’azienda alla necessità di integrazione vediamo alcune delle strategie più diffuse. 4.1.1 Scambio dei dati batch Immagine 16: Scambio di dati batch La prima strategia di integrazione è quella dello scambio di dati batch. Storicamente la tecnica dello scambio di dati in batch è stata la prima ad apparire. La tecnica si riduce principalmente al trasferamento dei dati attraverso scambio di files. I sistemi mainframe possono essere fatti operare durante la notte per l’estrazione dei dati, lo spostamento dei file su altri sistemi e il successivo caricamento degli stessi sul sitema target prima dell’inizio della giornata lavorativa. Sebbene quest’approccio possa sembrare semplicistico, presenta diversi vantaggi. L’utilizzo di file rappresenta un punto di disaccoppiamento tra I diversi processi. Se il sistema target non è in grado di ricevere immediatamente i file, questi possono essere salvati fino a che il sistema non diventa disponibile. L’astrazione a livello di file garantisce inoltre un disaccoppiamento robusto rispetto alla piattaforma tecnologica e al linguaggio tenendo come unica accortezza quella dell’utilizzo di un set di caratteri comune. 40 4 -­‐ L’architettura del sistema Allo stesso tempo lo scambio di dati in batch può presentare un certo numero di problematiche da tenere in considerazione. I cambiamenti di dati in un sistema potrebbero non essere disponibili nell’altro prima del giorno successivo. La cosa pone due problemi distinti: • Confusione per gli utenti: L’utente deve essere in qualche modo consapevole del fatto che sta lavorando con dati potenzialmente non aggiornati. • Problemi d’integrità dei dati e prestazionali: Potrebbero presentarsi situazioni in cui dati non aggiornati, sono inseriti in batch nel sistema. A livello prestazionale bisogna tenere anche conto del fatto che tali meccanismi tendono spesso a replicare l’intero dataset dal sistema di origine a quello di destinazione, ponendo potenzialemente dei problemi per quanto riguarda spreco di banda per la trasmissione. 41 4 -­‐ L’architettura del sistema 4.1.2 Database condiviso Immagine 17: Database condiviso Nel tentativo di eliminare I problemi di sincronizzazione e la replicazione di grandi quantità di dati, alcune aziende hanno incominciato a percorrere la via del database condiviso da più sistemi informatici. Problemi di sincronizzazione e di contesa della risorsa condivisa possono essere approcciati dall’uso di meccanismi di lock e protezione offerti da quasi tutti I DBMS. Il problema chiave di una soluzione del genere risiede nella difficoltà nel definire un modello dei dati che sia opportuno per tutte le applicazioni che lo condividono. Molte applicazioni sono costruite o acquistate con un modello di dati proprietario e il tentativo di fondere tutti questi modelli in uno comune si può rivelare un problema costoso e irrisolvibile. Un altro problema, anche se marginale, è quello legato alle performance: un database centralizzato rappresenterà da un certo momento in poi il collo di bottiglia in un sistema aziendale complesso. 42 4 -­‐ L’architettura del sistema Anche in questo caso, come per lo scambio di dati in batch, l’integrazione è legata al mero scambio di dati, e non di funzionalità. 43 4 -­‐ L’architettura del sistema 4.1.3 Scambio di dati destrutturati in streaming Immagine 18: Scambio di dati destrutturati in streaming Con il passare del tempo, è divenuto sempre più chiaro il fatto che l’approccio a database centrallizzato spesso non è una soluzione adottabile, specialmente quando il numero di pacchetti software specializzati cresce con la complessità dei processi aziendali. Una strategia per lo scambio di dati in maniera bidirezionale e interattiva è quella dell’utilizzo di protocolli di trasferimento di rete (TCP/IP Sockets). Il vantaggio principale delle soluzioni di questo tipo, risiede nel fatto che i dati possono essere propragati appena modificati nel sistema sorgente. Questo riduce la probabilità di avere un sistema non sincronizzato ed elimina la necessità di avere dei cicli di replicazione batch notturni. Le problematiche di questa soluzione non vanno però sottovalutate. Il trasferimento di dati avviene in maniera sincrona; questo significa che, quando i dati sono trasmessi attraverso il canale, il ricevente deve trovarsi in ascolto. Allo stesso tempo il sistema che trasmette attende dal ricevente una risposta per un periodo determinato di tempo, provocando una situazione di stallo. Per risolvere questo tipo di problematiche sono stati sviluppati sistemi raffinati come buffer e meccanismi basati sui tentativi succesivi per fornire comunicazioni affidabili su socket. 44 4 -­‐ L’architettura del sistema L’utilizzo di tali sistemi prevede inoltre che il flusso di dati sia completamente destrutturato. La conversione da byte a strutture dati adatte è tutta a carico degli sviluppatori, e tale processo può rivelarsi costoso e suscettibile ad errori. 4.1.4 Chiamata a procedura remota Immagine 19: Remote Procedure Call La chiamata a procedura remota (o RPC 25 ) è una strategia che ha come obbiettivo quello di disaccoppiare la logica di applicazione da quella di trasferimento dei dati in uno scenario distribuito, inserendo un layer intermedio. Tale layer si occupa in maniera trasparente di trasformare tipi di dati complessi in stream di dati come richiesto dal protocollo di trasporto sottostante. Come risultato il meccanismo RPC permette a un’applicazione l’invocazione trasparente di una funzione implementata in un’altra applicazione fisicamente dislocata in qualsiasi punto accessibile di una rete. Il meccanismo di serializzazione fa affidamento su stub generate in automatico a partire da una specifica di interfaccia definita in un IDL26 non legato ad una specifica piattaforma o tecnologia. Sebbene i sistemi basati su RCP rendano il processo d’integrazione molto più semplice, condividono con la tecnica dello scambio di dati destrutturati in streaming le problematiche dovute all’utilizzo di protocolli di comunicazione 25
Remote Procedure Call 26
Interface Definition Language 45 4 -­‐ L’architettura del sistema sincroni e inaffidabili. Inoltre l’utilizzo di questa strategia fa affidamento sul concetto di comunicazione “punto-­‐punto” tra mittente e ricevente che può diventare difficilmente manutenibile nello scenario in cui il numero di attori coinvolti aumenti. 4.1.5 Scambio di messaggi Immagine 20: Scambio di messaggi I sistemi di messaging cercano di superare i difetti delle precedenti soluzioni fornendo un’infrastruttura affidabile per il trasferimento asincrono dei dati. Un’applicazione può pubblicare i dati direttamente sul layer d’integrazione e avere l’assicurazione che tali dati arriveranno al destinatario (o ai destinatari). Il sistema mittente non deve attende in maniera sincrona una notifica del destinatario ma può consegnare il proprio messaggio al bus e continuare la propria elaborazione. I sistemi basati su messaggi spesso includono schemi d’indirizzamento che permettono di risolvere i problemi di mantenimento di molteplici connessioni “punto-­‐punto” tipiche dei sistemi RPC. Le caratteristiche appena presentate potrebbero portare a pensare a questi sistemi come quelli più adatti a una qualsiasi esigenza d’integrazione. In realtà anche questi ultimi presentano delle problematiche e delle sfide non indifferenti. I sistemi di messaging rappresentano la soluzione più giovane tra quelle presentate, e inquanto tali sono al centro di continui cambiamenti e 46 4 -­‐ L’architettura del sistema adattamenti. La progettazione di sistemi asincroni presenta problematiche completamente diverse da quelle dei sistemi classici. In alcuni casi compiti del tutto naturali e ben standardizzati in sistemi sincroni, come quelli del testing e del debugging, possono rappresentare degli scogli difficilmente sormontabili in questo nuovo scenario. 4.1.6 La soluzione adottata per il flusso d’importazione Nel software progettato e sviluppato l’esigenza d’integrazione non è così complessa da richiedere architetture di messaging. Il flusso di import consiste nell’importazione di tutti i dati aziendali utili al dominio della vendita all’interno del database della soluzione server sviluppata. Per quanto riguarda il flusso di import, l’esigenza fondamentale non è quella di consumare funzioni dell’ERP aziendali quanto quella di avere una fotografia giornaliera dei dati da distribuire ai client mobili. La strada che si è percorsa rappresenta un’evoluzione concettuale della prima strategia. Extract (DBMS database di interscambio, NO DBMS) Transform (Adattamento dei dati per essere accolte nel DB del server) Load (Caricamento dei dati trasformati all'interno del DB del server) Immagine 21: Processo ETL Il punto da cui estrarre i dati aziendali per l’integrazione è un database d’interscambio. Il database d’interscambio può essere fornito come una vista sul database dell’ERP aziendale. Nelle fasi preliminari d’integrazione è fornito un tracciato record cui fare riferimento per facilitare l’integrazione, 47 4 -­‐ L’architettura del sistema nonostante ciò il sistema è stato progettato con la possibilità di inserire dei mapping specifici tra i nomi delle entità sorgenti e quelle destinazioni. Lo schema utilizzato è quello classico di un processo ETL27. Possiamo parlare di un’integrazione batch suddivisa in tre fasi per ciascun’entità da importare: • Extract: La fase di estrazione consiste nell’esecuzione di una query sul database d’interscambio e nel caricamento in memoria di uno o più record. • Transform: La fase di trasformazione consiste nell’applicazione di operazioni sui dati di partenza come sostituzione di costanti, trim di stringhe, separazione di campi singoli su più campi etc… • Load: La fase di caricamento consiste nel persistere le strutture dati, opportunamente trasformate, sul database di destinazione. Nello specifico per l’implementazione si è utilizzato il framework Rhino ETL. Questo framework presenta una soluzione non molto comune rispetto ad i classici tool per processi ETL come SSIS28. Fornisce una struttura a pipeline per l’implementazione di un processo di questo tipo utilizzando gli strumenti classici della programmazione orientata agli oggetti (non mette a disposizione nessun tool grafico). 27
Extract, Transform, Load 28
Sql Server Integration Service 48 4 -­‐ L’architettura del sistema I concetti centrali del framework sono principalmente tre: • Riga: Rappresenta l’unità fondamentale di dati che è processata. E’ un semplice dizionario “chiave-­‐valore” • Operazione: L’operazione prende in ingresso una collezione di righe le consuma eventualmente modificandole e produce una collezione di righe. • Processo: Le operazioni possono essere combinate in una pipeline utilizzando un processo. L’output della prima operazione è processato come input della seconda e così via. Possiamo vedere come il processo di Rhino sia mappabile perfettamente con un processo ETL e come con le operazioni si possano implementare le fasi di estrazione e load. Per comprendere meglio com’è utilizzata l’infrastruttura, possiamo analizzare alcuni frammenti di codice che rappresentano un semplice processo. Un’operazione di extract corrisponde all’implementazione di una AbstractOperation. 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
public class EvenNumberToMillion : AbstractOperation { public override IEnumerable<Row> Execute(IEnumerable<Row> rows) { for(int i = 2; i< 1000000; i += 2) { Row row = new Row(); row["number"] = i; yield return row; } } } Snippet 7: Implementazione della prima AbstractOperation della pipeline 49 4 -­‐ L’architettura del sistema Com’è facile osservare il metodo Execute sovrascritto, prende in input una collezione di righe. In questo caso essendo l’operazione di extract la prima della pipeline, la variabile rows non arriverà inizializzata. Quello che viene fatto nel corpo della Execute è la generazione di tutti i numeri pari minori di 1’000’000. Tali numeri saranno inseriti nell’entità row e prodotti per il blocco successivo della pipeline in maniera lazy29. Se supponiamo che il nostro processo di traformazione consista in una moltiplicazione di un fattore due per i dati in precedenza estratti (o generati) potremmo creare un’operazione di questo tipo: 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
public class MultiplyNumber : AbstractOperation { public override IEnumerable<Row> Execute(IEnumerable<Row> rows) { foreach(Row row in rows) { row["number"] = row["number"] * 2; yield return row; } } } Snippet 8: Implementazione della seconda AbstractOperation della Pipeline In questo caso, trattandosi di un’operazione intermedia l’iterabile in ingresso sarà valorizzato. Nel metodo Execute è eseguita l’operazione di trasformazione estraendo il valore in arrivo dalla riga, moltiplicandolo per due e reinserendolo nella struttura dati. Anche in questo caso in uscita dall’operazione avremo un iterabile lazy. L’ultimo passaggio è quello del load. In questo caso si considera come operazione di load la semplice scrittura sullo standard output del valore elaborato della riga: 29
La parola chiave yield permette di ottenere delle strutture iterabili non precalcolate in cui gli elementi vengono prodotti appena qualcuno li consuma. 50 4 -­‐ L’architettura del sistema 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
public class PrintNumber : AbstractOperation { public override IEnumerable<Row> Execute(IEnumerable<Row> rows) { foreach(Row row in rows) { Console.Log(row["number"]); } } } Snippet 9: Implementazione dell'ultima AbstractOperation della pipeline L’ultima operazione di load a differenza delle altre non ha la parte di ritorno poichè rappresenta il punto finale dell’elaborazione. Il processo complessivo può quindi essere assemblato utilizzando i mattoni fondamentali appena scritti: 1.
2.
3.
4.
5.
6.
7.
8.
9.
public class NumbersProcess : EtlProcess { public override void Initialize() { Register(new EvenNumberToMillion()); Register(new MultiplyNumber()); Register(new PrintNumbers()); } } Snippet 10: Assemblaggio della pipeline con le varie operazioni In seguito può essere eseguito semplicemente istanziando una classe del processo e invocando il metodo Execute: 1.
2.
3.
4.
5.
6.
7.
8.
class Program { static void Main() { var process = new NumbersProcess(); process.Execute(); } } Snippet 11: Esecuzione della pipeline 51 4 -­‐ L’architettura del sistema Ovviamente l’implementazione reale della pipeline compie operazioni più complesse quali mapping dei campi, trasformazione dei tipi di dati, esecuzioni di query batch sia in estrazione sia in caricamento: 1. protected override void Initialize() 2. { 3. Register(new EtlExtractFromErpDbOperation( 4. _mappingService, 5. OperationParams)); 6. Register(new EtlTransformDateFromErpFormatOperation( 7. _dataFormatSettings, 8. _mappingService, 9. OperationParams)); 10. Register(new EtlTransformDateTimeFromErpFormatOperation( 11. _dataFormatSettings, 12. _mappingService, 13. OperationParams)); 14. Register(new EtlTransformRightTrimStringOperation( 15. _mappingService, 16. OperationParams)); 17. Register(new EtlTransformNullInvalidStringOperation( 18. _mappingService, 19. OperationParams)); 20. Register(new EtlDeletePreviousRecordsFromDbOperation( 21. OperationParams)); 22. Register(new EtlLoadToKimoDbOperation( 23. _mappingService, 24. OperationParams)); 25. } Snippet 12: Assemblaggio del processo d’importazione vero e proprio Sebbene il caso reale (il processo appena scritto) sia molto più complesso di quello presentato come esempio la struttura concettuale è la medesima. E’ facile capire come un’infrastruttura di questo tipo garantisca una libertà assoluta per quanto riguarda le esigenze più disparate d’integrazione. La flessibilità in fase di composizione della pipeline ETL è ancora più accentuata se si tiene conto del fatto che la soluzione utilizzata permette di utilizzare 52 4 -­‐ L’architettura del sistema come processi degli script scritti con un DSL30 che possono essere aggiunti a runtime senza necessità di ricompilare il progetto. 4.1.7 La soluzione adottata per il flusso di esportazione Il flusso di export consiste principalmente nell’esportazione dei documenti generati all’interno del database d’interscambio. A livello concettuale anche in questo caso si tratta di un processo ETL (tanto che è stato implementato con il medesimo framework). Da quel punto in poi l’importazione nel database vero e proprio dell’ERP sarà a carico dell’azienda. La scelta di lasciare l’ultimo pezzo d’integrazione a carico dell’azienda che intende installare il prodotto dipende dal fatto che non si possono fare assunzioni sul tipo di operazioni che andranno fatte sul documento arrivato. Con un accordo sul formato d’interscambio dei documenti basato su un tracciato record condiviso gli esperti dell’ERP aziendale possono con poco tempo implementare una strategia di importazione in maniera autonoma e personalizzata sulle proprie esigenze. Un generico documento è formato da record con i dati di testata e record con i dettagli: DocumentId ShippingCustomer BillingCustomer TotalDiscount ORD001 CUS0912 CUS0912 10 Tabella 2: Dati parzali di testata di un documento DocumentId ItemId Quantity Discount ORD001 PBA234 10 20 ORD001 PBA123 1 0 ORD001 PBA200 2 50 Tabella 3: Dati parziali delle righe di un documento 30
Domain Specific Language 53 4 -­‐ L’architettura del sistema La problematica specifica per quanto riguarda l’esportazione di questo tipo di entità con record multipli si più tabelle risiede nella necessità di transazionalita dell’operazione. A livello concettuale il database d’interscambio dovrebbe essere abbastanza avanzato da fornire un adeguato supporto nativo alle transazioni. Nella realtà ci si è scontrati con implementazioni non perfettamente funzionanti in sistemi legacy. In generale, non potendo imporre la tecnologia del database d’interscambio che spesso è una vista sul sistema informativo aziendale è stato necessario implementare un’algoritmo specifico che non fosse suscettibile agli errori. In generale quindi il database d’interscambio va considerato come “non capace di gestire le transazioni” per entità aggregate. La soluzione trovata per questo problema è stata quella di progettare un protocollo per la generica esportazione transazionale delle entità aggregate. Tale protocollo è simile a un classico commit a due fasi utilizzato nell’ambito delle transazioni distribuite, ma a differenza di questo non prevede un meccanismo di rollback della transazione quanto un meccanismo di recovery di situazioni di errore. 54 4 -­‐ L’architettura del sistema Immagine 22: Schema del protocollo di salvataggio delle entità Il protocollo utilizza le seguenti variabili di stato per la gestione della transazionalità: • Da esportare: Un’entità che deve essere esporta sarà marcata sul server con questo stato. Questo stato è valido solo per entità che risiedono sul server. • In esportazione: Un’entità per cui il processo di esportazione transazionale è iniziato sarà in questo stato. Questo stato è valido per entità che risiedono su server e database d’interscambio. 55 4 -­‐ L’architettura del sistema • In commit: Questo stato, legato solo alle entità sul server, corrisponde alla situazione in cui l’entità è stata trasferita completamente ma non si conosce l’esito dell’ultimo cambio di stato sul server. • Esportata: Un’entità che è stata esportata correttamente in maniera transazionale avrà il seguente stato sia sul server sia sul database d’interscambio. L’algoritmo sviluppato per l’esportazione transazionale fa affidamento sul fatto che il processo di esportazione è schedulato (nell’ordine dei minuti). La corretta esecuzione del processo da parte del coordinatore può essere vista come una successione dei seguenti passi: 1. Si trova nel database del server un’entità “da esportare”. 2. Viene cambiato lo stato da “da esportare” a “in esportazione” sul database del server. 3. L’entità viene trasferita sul database di interscambio mantenendo lo stato “in esportazione”. 4. Completato il trasferimento, viene cambiato lo stato dell’entità sul database del server da “in esportazione” a “in commit” 5. A questo punto viene cambiato lo stato dell’entità nel database di interscambio da “in esportazione” ad “esportata”. 6. Come ultimo passo viene cambiato lo stato dell’entità sul database del server da “in commit” a “esportata” Per dimostrare la bontà dell’algoritmo possiamo analizzare tutti i punti di fallimento del processo e analizzare come vedere come sia sempre possibile ripristinare il sistema in una condizione di validità al successivo scheduling dell’export: 56 4 -­‐ L’architettura del sistema • Errori durante l’operazione 2: Se non si riesce a cambiare lo stato da “da esportare” a “in esportazione” sul database del server il processo si ferma e alla successiva schedulazione è semplicemente ritentato. • Errori durante l’operazione 3 o 4 Se succede qualcosa di storto durante il trasferimento dell’entità, ci si trova nella situazione in cui lo stato dell’entità rimane “in esportazione” sul database del server e “in esportazione” sul database d’interscambio. Alla successiva schedulazione sarà trovata un’entità nel database del server con stato “in esportazione”. Questo scatenerà una procedura di pulizia dei dati sul database d’interscambio e verrà ritentata la procedura a partire dall’operazione 3. Anche nel caso in cui la procedura di pulizia dei dati dovesse incontrare problemi, si ricadrebbe nel medesimo caso attuale alla successiva schedulazione. Analogo il caso in cui non si riuscisse a cambiare lo stato dell’entità da “in esportazione” a “in commit”. • Errori durante l’operazione 5 o 6 Se qualcosa va storto nel passo 5 o nel passo 6 ci si ritrova nella situazione in cui l’entità sul database del server è “in commit”. In questo caso abbiamo la garanzia che il trasferimento sul db d’interscambio è avvenuto correttamente, non sappiamo se lo stato sul database di interscambio è stato correttamente messo a “esportato” ma la cosa non crea problemi. Non c’è necessità di pulire i dati nel database ma soltanto di impostare correttamente le entità allo stato finale rendendo quindi necessaria la ripetizione dell’algoritmo dal passo 5. 57 4 -­‐ L’architettura del sistema Imponendo la regola che il sistema informativo non deve prendere dal database d’interscambio nessuna entità che non abbia lo stato “esportato” si è ottenuto il comportamento voluto. 4.1.8 Accesso condiviso al database d’interscambio Un altro problema dell’approccio a database condiviso è quello dell’acceso da parte di due sistemi a una risorsa condivisa. Anche in questo caso l’impossibilità del controllo sulla tecnologia del database d’interscambio ha comportato la necessita dell’implementazione di una politica personalizzata per evitare collisioni in questo senso, non potendo fare affidamento su caratteristiche di alto livello fornite dal DBMS. Per capire l’approccio utilizzato è conveniente riepilogare come gli attori coinvolti possono accedere alla risorsa condivisa: • L’ERP aziendale può esportare i propri dati verso il database d’interscambio. • L’ERP può importare dati dal database d’interscambio. • L’integratore può importare i dati dal database d’interscambio nel database del server. • L’integratore può esportare dati dal database del server a quello d’interscambio. Anche in questo caso la divisione può essere fatta tra i due flussi principali di dati. Quello di import e quello di export. In particolare per quanto riguarda il flusso di dati in import le situazioni da evitare sono le seguenti: 58 4 -­‐ L’architettura del sistema Integratore importa i dati dal database di interscambio ERP esporta i dati verso il database di interscambio Immagine 23: Flusso di import. Le due frecce colorate rappresentano gli accessi al database che non devono avvenire contemporaneamente. • Si vuole evitare che l’integratore inizi a importare i dati dal database d’interscambio mentre l’ERP li sta esportando verso esso per non avere un’importazione di dati parziali. • Allo stesso tempo si vuole evitare che l’ERP inizi a esportare i propri dati sul database d’interscambio se l’integratore li sta importando L’analogo vale per il flusso di esportazione. L'integratore scrive i dati da esportare nel database di interscambio L'ERP importa i dati dal database di interscambio Immagine 24: Flusso di export. Le due frecce colorate rappresentano gli accessi al database che non devono avvenire contemporaneamente. Vale la pena notare che per come sono state progettate le strutture dati, non esistono entità che possono essere importate ed esportate. La scelta è stata fatta volutamente e si riflette per esempio nella gestione di entità diverse per i documenti che devono essere inseriti nell’ERP e quelli che provengono dall’ERP che sono individuati come documenti storici. Tale scelta ha permesso 59 4 -­‐ L’architettura del sistema di gestire le due problematiche (flusso d’import e di export) in maniera isolata. La concorrenza va gestita solo “intra flusso” e non “inter flusso”. La soluzione adottata è concettualmente basata sull’utilizzo di due lock per la gestione della mutua esclusione. • Lock per il flusso di import: Le operazioni di import dal database di interscambio al database del server e di esportazione dall’ERP al database di interscambio iniziano sempre con l’acquisizione del lock (salvato su una tabella apposita) e terminano sempre con il rilascio del lock. Nel caso non fosse possibile acquisire il lock, le richieste sono mantenute in un’apposita coda. • Lock per il flusso di export: Il comportamento è analogo a quello descritto per l’import ma coinvolge l’esportazione di entità dal database del server a quello di interscambio e l’importazione di dati da parte dell’ERP. La soluzione scelta è stata guidata dal principio di semplicità. I limiti di una soluzione di questo tipo sono ben noti e la scelta di non utilizzare tecniche più raffinate è data da un’analisi sul contesto in cui tale meccanismo viene utilizzato. Il problema principale di una soluzione del genere è dovuto al fatto che se il lock non viene correttamente rilasciato le operazioni vengono ritardate in maniera indeterminata. Mentre nell’ambito di algoritmi legati allo sviluppo di sistemi operativi questa problematica è molto sentita per quanto riguarda le prestazioni nell’ambito di un’integrazione di alto livello la cosa non crea problemi. La parte più critica in un sistema di questo tipo non è data dal fatto che l’integrazione avvenga in tempo reale, quanto piuttosto che durante il processo non si perda nessuna informazione. Il database del server e quello 60 4 -­‐ L’architettura del sistema dell’ERP da questo punto di vista funzionano come code soddisfacendo in maniera piena tale necessità. Le situazioni in cui i lock non sono rilasciati correttamente sono comunque loggate con la possibiltà di risoluzione automatica o di notifica via mail al personale tecnico. 61 4 -­‐ L’architettura del sistema 4.2 Il server Nel paragrafo precedente si è visto com’è stata affrontata la problematica dell’integrazione. A questo punto dell’architettura si è nella una situazione in cui i dati del sistema informativo dell’azienda sono stati importati nel server. Il server è l’unico punto di comunicazione con i fat client mobile. La comunicazione tra server e client mobile in questo caso è divisibile in due macroaree: • La sincronizzazione dei dati per lavorare anche sconnessi dalla rete. E’ un’operazione massiva, presuppone lo scambio di moli di dati sotto forma di file in formato binario compresso. • I servizi forniti ai client mobile in presenza di rete (data on demand real time, servizio di invio ordini, servizi messaggistica). Operazioni “leggere”, presuppongono lo scambio di messaggi serializzati. Sebbene le due aree presentino problematiche anche molto differenti, l’architettura scelta per soddisfare i requisiti è quella dei servizi web REST31. 4.2.1 Architettura REST Rest definisce un set di principi architetturali attraverso i quali progettare servizi web incentrati sul concetto di risorsa del sistema, inclusa la gestione di come gli stati della risorsa sono trasmessi grazie al protocollo HTTP32 ad un ampio range di client scritti in molti linguaggi diversi. Se si prende in considerazione il numero di servizi web che sono stati scritti negli ultimi anni, la quasi totalità è basata sul modello architetturale REST. L’impatto sul web, dovuto soprattutto alla semplicità e flessibilità dell’architettura, è stato così grande da soppiantare quasi del tutto l’utilizzo dei classici webservice SOAP33 31
REpresentational State Transfer 32
Hyper Text Transfer Protocol 33
Simple Object Access Protocol 62 4 -­‐ L’architettura del sistema basati sul linguaggio di definizione di interfacce WSDL34. Nella forma più pura un webservice REST segue quattro principi architetturali di base: • Utilizza esplicitamente i metodi HTTP. • É stateless • Espone URI con una struttura a directory • Traferisce messaggi come XML35, JSON36 o entrambi 4.2.2 Utilizzo esplicito dei metodi Http Una delle caratteristiche dei servizi web RESTful è l’uso esplicito dei metodi HTTP come definiti dall’RFC37 2616. Il metodo GET viene per esempio definito come un metodo di produzione dati che può essere usato da un client per ottenere risorse, per ottenere dati da un server, o per eseguire query aspettandosi che il server le utilizzi per estrarre e restituire le risorse individuate dalla query stessa. REST invita lo sviluppatore a utilizzare i metodi HTTP in maniera esplicita e in modo che l’utilizzo sia consistente con la definizione del protocollo. L’architettura base REST mappa in maniera univoca le classiche operazioni CRUD38 con i metodi HTTP: Operazione Metodo HTTP Creazione di una risorsa nel server POST Lettura e recupero di risorse dal server GET Cambiamento di stato o aggiornamento di risorse sul PUT server Cancellazione o rimozione di una risorsa sul server DELETE Tabella 4: Corrispondenza tra operazioni e metodi HTTP 34
Web Service Definition Language 35
eXtensible Markup Language 36
JavaScript Object Notation 37
Request For Comments 38
Create, Read, Update, Delete 63 4 -­‐ L’architettura del sistema Uno dei problemi principali che può essere osservato nelle più diffuse API39 web è l’utilizzo improprio dei metodi HTTP. L’URI della richiesta in una chiamata GET per esempio dovrebbe identificare una risorsa specifica. La stringa di query in un URI di richiesta dovrebbe includere una serie di parametri che definiscono i criteri di ricerca con cui il server deve cercare la risorsa. Esistono però molti casi in cui la specifica non viene in alcun modo rispettata. Ci sono per esempio API in cui una richiesta HTTP GET scatena un’operazione transazionale sul server come la scrittura di dati su un database. Un tipico esempio di API HTTP sviluppata in maniera non aderente allo standard può essere rappresentato da una richiesta così formata: HTTP Method Query String Protocol GET /addbook?title=CavesOfSteel HTTP/1.1 Tabella 5: Esempio di GET non corretta Una get di questo tipo rappresenta un’operazione con cambio di stato, è una’operazione con effetti colalterali. Se correttamente processata il risultato è l’aggiunta di un nuovo libro all’interno dello store di dati sottostante. I problemi di un servizio del genere sono su due livelli: • Semantico: L’operazione che è dichiarata come GET non esegue affatto operazioni legate al concetto di GET della specifica HTTP. • Tecnico: Il fatto che sia possibile scatenare attraverso una GET operazioni con side effect rende in sostanza impossibile applicare una cache trasparente. Ancora più grave la situazione in cui un crawler di un motore di ricerca si possa trovare nella situazione di cambiare 39
Application Programming Interface 64 4 -­‐ L’architettura del sistema inavvertitamente lo stato di un webserver semplicemente seguendo un collegamento ipertestuale. Un approccio corretto in questo caso consisterebbe nell’incapsulamento della richiesta in un messaggio di tipo POST: HTTP Query Method String POST /books Protocol Body HTTP/1.1 <?xml version=”1.0”?> <book> <title>CavesOfSteel</title> </book> Tabella 6: Esempio di POST corretta Si utilizza il metodo POST sulla risorsa books e si specifica nel payload la risorsa che si vuole creare sul server. Lato ricevente la richiesta può essere processata aggiungendo la risorsa specificata nel body come subordinata della risorsa identificata nell’URI della richiesta. Nel caso specifico la risorsa sarà aggiunta come figlia di /books. Il legame di parentela tra la nuova entità e il genitore, come specificato dalla richiesta POST, è analogo a quello tra un file e la directory che lo contiene. A questo punto un client potrebbe volere una rappresentazione della risorsa utilizzando il nuovo URI e potrebbe semplicemente utilizzare una GET di questo tipo: HTTP Method Query String Protocol GET /books/CavesOfSteel HTTP/1.1 Tabella 7: Esempio di GET corretta 65 4 -­‐ L’architettura del sistema L’uso del metodo GET in questo caso è esplicito poichè, come da specifica, è utilizzato solo per il recupero di una risorsa dal server. Un caso simile a quello della GET impropriamente usata per la creazione di risorse può essere visto per quanto riguarda l’update: HTTP Method Query String Protocol GET /updatebook?title=CavesOfSteel&
HTTP/1.1 newtitle=TheCaveOfSteel Tabella 8: Ulteriore esempio di GET non corretta In questo caso la richiesta GET maschera in realtà la volontà di aggiornare il libro dal titolo CavesOfSteel cambiandolo in TheCaveOfSteel. Anche in questo caso l’utilizzo improprio deriva dal fatto che si sta utilizzando la query string come “firma del metodo da invocare”. Anche in questo caso esiste una soluzione esplicita nello standard HTTP per operazioni di questo tipo, l’uso del metodo PUT. HTTP Method Query String Body PUT /books/CavesOfSteel <?xml version=”1.0”?> <book> <title>TheCaveOfSteel</title> </book> Tabella 9: Esempio di PUT corretta L’utilizzo del metodo PUT fornisce un’interfaccia molto più pulita e consistente con i principi architetturali dei webservice REST. La richiesta esplicita può essere spiegata in questo modo: il client richiede di modificare la risorsa /books/CavesOfSteel trasferendo il nuovo stato della risorsa all’interno del 66 4 -­‐ L’architettura del sistema corpo del messaggio invece di passare un set arbitrario di nomi di parametri e valori dirrettamente dall’URI della richiesta. L’utilizzo di questo metodo ha un importante effetto collaterale. La risorsa è rinominata da CavesOfSteel a TheCaveOfSteel, e di conseguenza viene modificato l’URI della risorsa in /books/TheCaveOfSteel. Dal momento in cui la risorsa è stata rinominata, tutte le chiamate al vecchio URI della risorsa genereranno in maniera automatica l’errore standard 40440 . Come regola quando si progetta un’API REST si dovrebbero sempre utilizzare nomi negli URI e non verbi. Le azioni esplicitate dai verbi nel caso di servizi REST sono già definite dal protocollo (POST, GET, PUT e DELETE). Per mantenere un’interfaccia generalizzata, e per permettere al client di essere espliciti riguardo alle operazioni invocate, il servizio web non dovrebbe definire verbi o procedure remote come /addbook o /updatebook. Il principio architetturale generale si applica anche al corpo della richiesta HTTP, che è dedicato al trasferimento dello stato della risorsa, e non rappresenta un contenitore per il nome di una procedura remota da invocare. 4.2.3 Stateless vs Statefull Una caratteristica fondamentale dei servizi REST è di scalare in maniera efficiente rispetto ad aumenti di richieste. Sebbene questa non sia una necessità sentita nel nostro scenario, rappresenta comunque un punto da analizzare. Cluster di server, con bilanciamento di carico e capacità di failover, proxy e gateway sono tipicamente organizzati secondo una topologia che permette di propagare le richieste da un server all’altro sulla base del carico in modo da minimizzare il tempo di risposta di una chiamata web. La possibilità di scalare utilizzando più server ha come prerequisito il fatto che il client invii 40
Not Found Error 67 4 -­‐ L’architettura del sistema richieste REST complete e indipendenti; con complete e indipendenti s’intende che tutto lo stato necessario alla computazione deve essere fornito dal client in modo che la richiesta possa essere passata a un qualsiasi altro server senza dover mantenere nessun dato intermedio su nessun server specifico nella topologia. Una richiesta completa e indipendente non ha bisogno che il server recuperi alcuno stato o contesto. Un client REST generalmente deve avere nell’header e nel body della risposta tutti i parametri, il contesto ed i dati necessari al server a generare una risposta. Tale scelta di design oltre ad aumentare la scalabilità come già detto, semplifica molto l’architettura del server perchè rende superflua la necessità di inserire codice per la gestione della sincronizzazione dei dati di sessione. Si possono vedere con un esempio diretto le due casistiche considerate. Considerando la situazione in figura abbiamo il classico esempio di servizio statefull. Si osservi una situazione in cui un’applicazione può richiedere la risorsa successiva in un risultato “multipagina”. Immagine 25: Servizio web statefull Nel caso del servizio statefull la richiesta sarà semplicemente di avere la prossima pagina del risultato. Il server dovrà avere in memoria il contesto in cui è stata fatta la richiesta per risolvere il significato di “prossima”. Dovrà 68 4 -­‐ L’architettura del sistema sapere quale pagina il client specifico stava visualizzando. Com’è facile capire un design di questo tipo non permette di spostare la richiesta da un server a un altro in maniera trasparente, se non trasferendo e sincronizzando anche il contesto, con tutte le complicazioni del caso. D’altro canto un’architettura stateless permette di ottenere tutti i vantaggi di cui abbiamo parlato in maniera trasparente. Immagine 26: Servizio web stateless In questo caso, come possiamo vedere in figura, nella richiesta sono presenti tutti i dati che permettono al server di poter computare la richiesta senza mantenere informazioni di contesto. Il client dice al server in maniera esplicita che richiede la pagina due e riceve indietro la risorsa con un riferimento esplicito alla pagina successiva se presente. In questo modo se il client avrà bisogno della pagina successiva, farà semplicemente una richiesta esplicita grazie al riferimento dato dal server. In questo modo la richiesta può essere inoltrata lungo la topologia di server a piacere senza nessuna necessità di sincronizzare dati di contesto. Com’è chiaro dall’esempio un design di questo tipo che implica una collaborazione tra client e server rende molto più semplice la gestione server side, aumenta la scalabilità e garantisce un’ottimizzazione generale a livello di performance. Sebbene come già detto le performance nello scenario di 69 4 -­‐ L’architettura del sistema un’applicazione business to business non siano centrali, la semplificazione lato server rende giustificata la scelta di tale approccio. 4.2.4 Esposizione di URI con struttura a directory Dal punto di vista del client che deve accedere alle risorse, la struttura degli URI determina la semplicità e l’intuitività dell’API e i modi con cui si può interagire con i servizi. Una buona parte della qualità di un’API REST è determinata dalla progettazione della struttura degli URI. Gli URI di servizi web REST dovrebbero essere il più possibile “autoesplicativi”. Dovrebbe essere semplice capire ciò che fanno semplicemente leggendoli. Possiamo vedere nella seguente tabella come tali principi hanno guidato lo sviluppo delle API esposte guardandone alcune: Method URI Risultato GET Restituisce il profilo utente in base /user/profile alle credenziali inserite nell’header standard HTTP. GET /setup/ipad Restituisce l’ultima versione disponibile dell’eseguibile per ipad. Nel body della get viene richiesta la versione del client richiedente. Questa, unita alle credenziali nell’header permettono l’implementazione di policy personalizzate. GET /syncmanifest Restituisce un manifest dello stato dei dati di sincronizzazione per il 70 4 -­‐ L’architettura del sistema client mobile. Tra le altre info ritorna l’ID dello sincronizzazione stato cui di fare riferimento per raccogliere i dati. GET /entities/{syncId}/ Restituisce i dati di sincronizzazione {entityName} per una specifica entità per una specifica sincronizzazione (il dato è preso dal manifest). POST /document Crea una risorsa documento passata nel body nel database del server. GET /document/ Restituisce un documento creato {documentId} con uno specifico identificatico o un errore 404 se il documento non esiste. POST /importprocess Avvia il processo d’importazione dall’erp al server e ritorna l’ID se il processo è stato fatto partire correttamente. GET /importprocess/{id} Restituisce lo stato del processo d’importazione dall’ERP al server. Tabella 10: API parziale dell'applicazione sviluppata Com’è possibile notare l’API risultante è abbastanza chiara e autoesplicativa. 4.2.5 Trasferimento dei dati in JSON o XML La rappresentazione della risorsa tipicamente riflette lo stato attuale della risorsa e dei suoi attributi nel momento in cui il client la richiede. La rappresentazione delle risorse spesso si riduce a una loro fotografia nel tempo. Come tali dati siano mappati a quelli su un database relazionale o a 71 4 -­‐ L’architettura del sistema entità del modello di dominio, non comporta nessuna differenza dal punto di vista di un servizio REST. L’unico vincolo che è dato per il design di servizi di questo tipo è il formato con cui sono scambiate tali rappresentazioni dello stato all’interno del corpo delle richieste e risposte HTTP. In generale i fomrati che possono essere utilizzati sono i seguenti: MIME-­‐Type Content-­‐Type JSON application/json XML application/xml XHTML application/xhtml+xml Tabella 11: Tipi di contenuto per il body dei messaggi HTTP Ovviamente il formato d’interscambio va dichiarato in maniera corretta nell’header del messaggio HTTP. La scelta del formato d’interscambio nel caso dell’applicazione sviluppata non pone problemi particolari grazie all’uso di una libreria che astrae il concetto di formato di serializzazione ma nello specifico la configurazione scelta è ricaduta sull’uso del JSON. 1. { 2. "UserId": "010", 3. "SyncId": "20110909-­‐1132", 4. "Entities": [{ 5. "EntityName": "Customer", 6. "SyncGroup": "0", 7. "CompressedFileMd5Sum": "92df5840562c72d60868a41b429fde8f", 8. "UncompressedFileMd5Sum": "b941ed1c972b25cc4f10fa71a841f7b9" 9. }, { 10. "EntityName": "Item", 11. "SyncGroup": "0", 12. "CompressedFileMd5Sum": "2b72ab55cec4479b1b99cefbbe76ed5c", 13. "UncompressedFileMd5Sum": "9a2fdf7e5ff60accf67dd5110b0f8feb" 14. }, { 15. "EntityName": "ItemCost", 72 4 -­‐ L’architettura del sistema 16. "SyncGroup": "0", 17. "CompressedFileMd5Sum": "242738693210a3d3bb30d09209bb19ec", 18. "UncompressedFileMd5Sum": "c2dd8bd0b8aedf14240c358ec49a41ef" 19. }] 20. } Snippet 13: Manifest di sincronizzazione in formato JSON 1. <?xml version="1.0" encoding="UTF-­‐8" ?> 2. <Sync> 3. <UserId>010</UserId> 4. <SyncId>20110909-­‐1132</SyncId> 5. <Entities> 6. <EntityName>Customer</EntityName> 7. <SyncGroup>0</SyncGroup> 8. <CompressedFileMd5Sum>92df5840562c72d60868a41b429fde8f 9. </CompressedFileMd5Sum> 10. <UncompressedFileMd5Sum>b941ed1c972b25cc4f10fa71a841f7b9 11. </UncompressedFileMd5Sum> 12. </Entities> 13. <Entities> 14. <EntityName>Item</EntityName> 15. <SyncGroup>0</SyncGroup> 16. <CompressedFileMd5Sum>2b72ab55cec4479b1b99cefbbe76ed5c 17. </CompressedFileMd5Sum> 18. <UncompressedFileMd5Sum>9a2fdf7e5ff60accf67dd5110b0f8feb 19. </UncompressedFileMd5Sum> 20. </Entities> 21. <Entities> 22. <EntityName>ItemCost</EntityName> 23. <SyncGroup>0</SyncGroup> 24. <CompressedFileMd5Sum>242738693210a3d3bb30d09209bb19ec 25. </CompressedFileMd5Sum> 26. <UncompressedFileMd5Sum>c2dd8bd0b8aedf14240c358ec49a41ef 27. </UncompressedFileMd5Sum> 28. </Entities> 29. </Sync> Snippet 14: Manifest di sincronizzazione in formato XML Da come possiamo vedere il formato JSON è estremamente più compatto e facilmente leggibile rispetto ad una rappresentazione XML. Sebbene la minimizzazione della quantità di dati trasferiti sia comunque ben accetta, il 73 4 -­‐ L’architettura del sistema vantaggio principale è legato alla leggibilità. La possibilità di scrivere semplicemente il corpo del messaggio in un file di log senza dover mantenere codice per parsing e “pretty printing” dell’XML rappresenta da solo un motivo valido per la scelta. Un altro vantaggio in ottica futura è dovuto al fatto che il formato JSON è mappato in maniera diretta con l’object model del linguaggio Javascript. Nello scenario di sviluppo di un client web (applicazione in javascript che gira nel browser) tale scelta è sicuramente la più indicata. 4.2.6 Sicurezza Per quanto riguarda la sicurezza dello strato dei servizi web bisogna considerare due problematiche ben distinte: • Autenticazione: L’autenticazione è il meccanismo che permette di capire chi sta facendo una richiesta per una risorsa. • Confidenzialità: La confidenzialità riguarda il fatto che lo scambio di messaggi tra il client ed il server non deve essere in alcun modo intercettato e letto da parte di entità terze. Sebbene le due problematiche siano molto importanti in uno scenario in cui sono scambiati dati sensibili tra client e server alcune assunzioni riguardo al contesto del software hanno permesso di semplificare molto la gestione. Vale la pena ricordare che lo scenario di utilizzo è quello di un rapporto tra azienda e un agente di commercio (o al limite direttamente con un’altra azienda di cui è fornitrice) con un ventaglio di possibilità che spazia tra un rapporto tra “datore di lavoro” e “subordinato” allo scenario “business to business”. In 74 4 -­‐ L’architettura del sistema entrambi i casi, i servizi offerti non saranno mai esposti in maniera pubblica sul web. Tale semplificazione ha permesso di risolvere i problemi di confidenzialità imponendo un primo strato basato su una VPN41 sicura per accedere ai servizi ed un secondo strato legato all’identificazione del chiamante basato sull’insicura ma estremamente semplice autenticazione HTTP standard. Immagine 27: Schema VPN + tunnel SSL Nell’immagine vediamo sulla sinistra il server della soluzione sviluppata all’interno della LAN 42 dell’azienda. Il client per accedere al server dovrà connettersi al VPN server/firewall opportunamente configurato per cifrare il traffico tra i due punti attraverso la creazione di un tunnel SSL43. L’autenticazione base HTTP utilizza le credenziali nell’header del messaggio di richiesta. Per farlo si utilizza semplicemente il campo authorization in cui sono inserite le credenziali concatenate con il carattere “:” e convertite in Base64. Username Password Concatenate Base64 user pwd user:pwd dXNlcjpwd2Q= Tabella 12: Composizione e codifica di username e password in Base64 41
Virtual Private Network 42
Local Area Network 43
Secure Socket Layer 75 4 -­‐ L’architettura del sistema Se le credenziali non arrivano con una richiesta, il server risponderà con un errore 401 richiedendo al client le credenziali. Sarà a carico suo ricreare la richiesta compilando a dovere i campi necessari. L’encoding in Base64 non rappresenta in nessun modo una protezione per quanto riguarda le credenziali ma assicura solo che i caratteri spediti non vadano in conflitto con nessn tipo di carattere riservato. Le credenziali sono spedite in plain text e, come già detto, la confidenzialità è garantita dal meccanismo a monte di VPN + tunnell SSL. 76 4 -­‐ L’architettura del sistema 4.3 Architettura del core dell’applicazione client In questo paragrafo si analizzeranno le scelte di design che hanno guidato lo sviluppo del client su piattaforma iPad. Come già detto, l’applicazione presa in considerazione è un FAT client e come tale incorpora tutti gli strati di un’ideale architettura a tre strati. Presentation Business Logic Data Access Immagine 28: Architettura a tre tier del fat client Il data access layer è la parte del sistema che si occupa della persistenza delle entità di dominio. Il layer di business logic è quello in cui le entità collaborano al raggiungimento degli obiettivi dell’utente. Lo strato di presentazione include tutta la logica necessaria a mostrare a schermo i risultati ottenuti dal layer di business e quella per propagare gli input dell’utente. Vedremo layer per layer le scelte che hanno permesso di costruire componenti disaccoppiati e facilmente intercambiabili. 77 4 -­‐ L’architettura del sistema 4.3.1 Data Preparation Prima di entrare nelle scelte progettuali e implementative del data layer vale la pena fare una ricapitolazione di alto livello dello stack software nel suo insieme non limitando il campo visivo al client mobile. Nei paragrafi precedenti si è visto come i dati fluiscono dall’ERP aziendale alla soluzione server sviluppata e come il server può offrire servizi di tipo REST ai vari client. Si è parlato del fatto che il server offre dei servizi di sincronizzazione al client, ma non si è detto in maniera organica cosa significa fare una sincronizzazione. La spiegazione dell’algoritmo completo esula dallo scopo della trattazione ma anche in questo caso si tratta di uno scambio di dati in batch. Client Mobile Agente 001 DB Server Client Mobile Agente 002 Client Mobile Agente 003 Preparazione Dati Sincronizzazione Dati Immagine 29: Processo di preparazione e sincronizzazione dei client mobili Il server attraverso l’utilizzo di filtri specifici (per agenti e altri criteri) è in grado di sezionare orizzontalmente e verticalmente i dati da preparare per i 78 4 -­‐ L’architettura del sistema singoli client mobile specifici. Nello specifico i dati sono preparati per singola entità e mantenuti con un manifest di metadati. La scelta di separare i file per singola entità è dovuta al fatto che le entità più corpose come le anagrafiche hanno cicli di modifica generalmente molto lunghi e, mantenedo un semplice hash del pacchetto dati generato nel manifest, è possibile evitare reimportazione e sincronizzazione lato client risparmiando banda e accorciando in maniera drastica i tempi di sincronizzazione del client. Un’algortimo di sincronizzazione di questo tipo per quanto semplice ha permesso di ottenere tempi di sincronizzazione migliori delle aspettative dei clienti. L’analisi e l’ottimizzazione dell’algoritmo, compiuta profilando il processo in diversi scenari, hanno permesso di ottenere i segenti risultati. Caso Tempo medio per Tempo medio per aziende con anagrafiche aziende con anagrafiche Prima corpose ridotte Circa 100’000 articoli Circa 10’000 articoli 100 s 20 s 12 s 12 s sincronizzazione Sincronizzazioni Successive Tabella 13: Tempi di sincronizzazione. I risultati considerati sono da leggere considerando il collo di bottiglia in termini di banda dovuto all'utilizzo di adsl con 1Mbit in upload sul server di test Tali risultati sono stati misurati in maniera non rigorosa poichè i risultati ottenuti sono stati sin da subito soddisfacenti e non si è andati oltre nell’analisi e nell’ottimizzazione che potrà essere fatta solo se si presenterà uno scenario in cui diventerà necessaria. 79 4 -­‐ L’architettura del sistema 4.3.2 Data Access Una volta avvenuta la sincronizzazione sul client, si ha una situazione classica con logica di business che deve accedere a un database relazionale 44 . Ricordando che uno degli obiettivi del progetto è di avere un client multipiattaforma si è scelto di rendere intercambiabile la parte di accesso vero e proprio al DB. Per ottenere tale risultato si è scelto di implementare un DAL45 utilizzando il Repository Pattern. Un DAL è del codice di libreria che fornisce l’accesso a dati contenuti all’interno di uno storage persistente. Nel sistema a strati in considerazione, al DAL è delegata la responsabilità di leggere e escrivere sullo storage persistente. I requisiti che hanno guidato la progettazione del DAL sono i seguenti: • Indipendenza dal database: Il DAL deve essere l’unico punto del sistema in cui sono conosciute e sono utilizzate stringhe di connesione e nomi dei campi delle tabelle. L’indipendenza in questo caso significa che tale strato deve nascondere la tecnologia sottostante offrendo solo un’interfaccia di alto livello che permetta le operazioni di tipo CRUD. All’osservatore esterno deve sembrare una scatola nera attaccata al sistema che garantisce scrittura e lettura su un qualsiasi DBMS o storage persistente di sorta. • Configurabilità a plugin Per configurabilità a plugin s’intende la possibilità di cambiare attraverso un file di configurazione l’implementazione dello specifico 44
Su iPad l’unico DBMS relazionale utilizzabile è quello fornito dalla piattafomra: SQLITE 45
Data Access Layer 80 4 -­‐ L’architettura del sistema repository. Deve essere possibile per esempio supportare un nuovo DBMS con due sole operazioni logiche: la scrittura di un componente specifico per il nuovo DBMS e il cambiamento di un file di configurazione per far caricare a runtime il nuovo componente. Da un punto di vista prettamente funzionale le caratteristiche offerte da un DAL devono essere le seguenti: • Servizi CRUD I servizi CRUD sono una serie di metodi che permettono di persistere oggetti nel database relazionale e viceversa, caricando i risultati all’interno di nuove istanze delle classi del modello di dominio. I servizi CRUD devono funzionare sia per oggetti transienti sia per oggetti persistenti. Gli oggetti persistenti sono quelli che derivano dalle informazioni estratte dal database, quelli transienti sono quelli che sono creati in memoria per essere in seguito persistiti. • Servizi query: I servizi di tipo query servono per tutti quei casi in cui si vogliono compiere operazioni sul database più complesse delle semplici CRUD. Spesso per motivi prestazionali (o di praticità) è conveniente spostare la complessità sulla query vera e propria invece di demandare il compito ad un lavoro in memoria successivo. • Gestione della transazionalità In un’applicazione enterprise che fa dell’estrazione e manipolazione di dati una delle attività più comuni, non si vuole che ogni volta che è fatta una query sia creata una connessione, aperta, estratti i dati e in seguito chiusa. Una politica del genere genererebbe un traffico molto elevato verso il database. Nello sviluppo di applicazioni è considerata una buona norma minimizzare tale traffico. Allo stesso tempo bisogna 81 4 -­‐ L’architettura del sistema fornire dei meccanismi per la gestione transazionale di operazioni su entità differenti. Le problematiche e i requisiti appena espressi sono stati risolti utilizzando un approccio a repository. 4.3.3 Repository Pattern Come sappiamo il modello di dominio contiene classi che incapsulano dei dati ed espongono comportamenti. L’estrazione e il salvataggio di tali entità sono a carico dei repository. Ciascun’entità di dominio avrà il proprio repository. In generale per rendere indipendente l’implementazione per il DBMS specifico sono state create delle interfaccie. Tutte le classi che utilizzano i repository lo fanno sempre attraverso una loro interfaccia e mai istanziandole direttamente. Immagine 30: Interfaccie dei repository di alcune delle entità di dominio 82 4 -­‐ L’architettura del sistema Tale scelta permette di soddisfare in pieno il requisito d’indipendenza dal database. Il requisito di configurabilità a plugin è stato ottenuto attraverso l’impiego di un IOC46 container per applicare tecniche di dependency injection e late binding. E’ possibile vedere per esempio l’interfaccia del repository di documenti che presenta alcune particolarità trattandosi di un’entità composta di un oggetto di testata e un aggregato di linee del documento. 1. using Kimo.Model.Entities; 2. using Kimo.Model.Entities.Documents; 3. 4. namespace Kimo.Model.RepositoriesInterfaces 5. { 6. public interface IDocumentRepository 7. { 8. IDocument GetById(DocumentFamilyId documentFamilyId, string id); 9. 10. IDocument GetOrNullById(DocumentFamilyId documentFamilyId, string id); 11. 12. bool Exists(DocumentFamily documentFamily, string id); 13. 14. void SaveWholeDocument(IDocument document); 15. 16. void DeleteById(DocumentFamilyId documentFamilyId, string documentId); 17. } 18. } Snippet 15: Interfaccia del repository di documenti In questo caso si può vedere che tale repository offre classici servizi di estrazione di documenti, un servizio per testare l’esistenza di un determinato documento di una specifica famiglia, dei servizi per persistere un documento nella sua interezza e per persistere solo la testata e un servizio per la cancellazione del documento. 46
Inversion Of Control 83 4 -­‐ L’architettura del sistema Analizzando adesso l’implementazione specifica per un classico dbms che utilizza SQL standard, possiamo vedere come tale repository si comporta: 1. using System; 2. using Kimo.Data.Sql.DataContext; 3. using Kimo.Model.Entities; 4. using Kimo.Model.Entities.Documents; 5. using Kimo.Model.RepositoriesInterfaces; 6. 7. namespace Kimo.Data.Sql.Repositories 8. { 9. public class DocumentRepository : BaseRepository, IDocumentRepository 10. { 11. private readonly IDocumentLineRepository _documentLineRepository; 12. private readonly IDocumentDataRepository _documentDataRepository; 13. 14. 15. public DocumentRepository( 16. ISqlDataContextProvider dataContextProvider, 17. IDocumentLineRepository documentLineRepository 18. IDocumentDataRepository documentDataRepository) 19. : base(dataContextProvider) 20. { 21. _documentLineRepository = documentLineRepository; 22. _documentDataRepository = documentDataRepository; 23. } Snippet 16: Costruttore del DocumentRepository Per prima cosa notiamo come tale repository eredita alcune funzionalità da un BaseRepository e come detto implementa l’interfaccia IDocumentRepository. Come possiamo vedere il repository accetta come parametro in ingesso un DataContextProvider. Tale oggetto è quello centrale per fare le vere e proprie query al DB. Il datacontext provider è passato al costruttore della classe base che in questo caso non fa nient’altro che risolvere il DataContext. 84 4 -­‐ L’architettura del sistema 1. public abstract class BaseRepository 2. { 3. private readonly ISqlDataContextProvider _dataContextProvider; 4. protected readonly ISqlDataContext DataContext; 5. 6. protected BaseRepository(ISqlDataContextProvider dataContextProvider) 7. { 8. _dataContextProvider = dataContextProvider; 9. DataContext =(ISqlDataContext)_dataContextProvider.GetDataContext(); 10. } 11. } Snippet 17: Classe Base per i repository Le altre dipendenze IDocumentLineRepository del e DocumentRepository un sono IDocumentDataRepository. un Questa particolarità è dovuta al fatto che come già detto il documento è un oggetto composto che aggrega al suo interno altre entità. Per non duplicare logica di persistenza si delega al repository delle linee e dei dati di testata del documento la responsabilità di persistenza ed estrazione. E’ possibile vedere come l’implementazione del metodo d’interfaccia SaveWholeDocument deleghi il salvataggio ai due repository iniettati dal costruttore: 1. public void SaveWholeDocument(IDocument document) 2. { 3. var docType = DocumentFamilyManager.GetDocumentFamilyIdBy(document); 4. DeleteById(docType, document.Id); 5. SaveHeader(document); 6. SaveLines(document); 7. } 8. private void SaveHeader(IDocument document) 9. { 10. var documentFamily = DocumentFamilyManager.GetFamilyBy(document); 11. _documentDataRepository.Save(DataContext,documentFamily,document); 12. } 85 4 -­‐ L’architettura del sistema 13. private void SaveLines(IDocument document) 14. { 15. var documentFamily = DocumentFamilyManager.GetFamilyBy(document); 16. foreach (var documentLine in document.Lines) 17. { 18. _documentLineRepository.Save(documentLine, documentFamily); 19. } 20. } Snippet 18: Document Repository SaveWholeDocument Per arrivare alle query vere e proprie sul database basterà arrivare nei due repository iniettati. 1. public void Save( 2. ISqlDataContext dataContext, 1. public void Save( 2. ISqlDataContext dataContext, 3. DocumentFamily documentFamily, 4. IDocument document) 5. { 6. string tableName = documentFamily.GetTableName(); 7. var command = dataContext.CreateCommand( 8. "INSERT INTO @tablename (" 9. + " Id," 10. + " DocumentFamilyId," 11. + " DocumentTypeId," 12. + " CreatorId," 13. // + ... + 14. + " Notes," 15. + " Status" 16. + ") VALUES (" 17. + "@Id," 18. + "@DocumentFamilyId," 19. + "@DocumentTypeId," 20. + "@CreatorId," 21. // + ... + 22. + "@Notes," 23. + "@Status)"; 24. 25. 86 4 -­‐ L’architettura del sistema 26. AddAndSetParameter(command, "@Id", document.Id); 27. AddAndSetParameter(command,"@DocumentFamilyId", document.DocumentFamilyId); 28. AddAndSetParameter(command, "@DocumentTypeId",document.DocumentTypeId); 29. AddAndSetParameter(command, "@CreatorId", document.CreatorId); 30. // ... 31. AddAndSetParameter(command, "@Notes", document.Notes); 32. AddAndSetParameter(command, "@Status", document.Status); 33. dataContext.ExecuteCommand(command); 34. } Snippet 19: Metodo Save del DocumentDataRepository(vari campi sono stati omessi per otivi di brevità) Con questo esempio abbastanza complesso si può vedere come la query vera e propria sia eseguita in una zona ben confinata e nascosta agli utilizzatori del repository, che accedono ai suoi servizi attraverso un’interfaccia. Si può vedere come l’interfacciamento diretto con il database avvenga attraverso l’uso del DataContext che è creato da un DataContextProvider. In questo modo sono stati ottenuti due livelli di disaccoppiamento: • Disaccoppiamento tra repository e strato di persistenza: Il dominio non sa e non saprà mai come e dove lo strato sottostante implementa la logica di persistenza delle entità. Un repository potrebbe essere reimplementato per utilizzare webservice o scrittura grezza su file; per lo strato successivo non ci sarebbe nessun cambiamento. • Disaccoppiamento tra repository specifici SQL e DBMS utilizzato: Il confinamento dell’interfacciamento con il database nel DataContext riesce a garantire l’intercambiabilità completa del DBMS sottostante con il solo requisito che sia in grado di eseguire query SQL standard. Tale disaccoppiamento è quello che permette di far girare lo stesso 87 4 -­‐ L’architettura del sistema codice dei repository sia su sqlite su iPad che su SQL Server su microsoft Windows. La gestione della transazionalità è un’altro dei problemi delegati al DataContext. Nell’analisi in quello che abbiamo chiamato Request/Response Layer vedremo come tale problematica sia stata affrontata. 4.3.4 Request Reponse Layer L’utilizzo del repository pattern per quanto riguarda la persistenza dei dati pone alcune domande non banali. Quella più interessante è quella della gestione della transazionalità. Non è possibile e non è corretto pensare che ogni accesso a un repository apra e chiuda una transazione nel database. L’esigenza più comune è comunemente quella di incapsulare in una transazione un insieme di operazioni che accedono in lettura e scrittura a più repository. Se supponiamo per esempio che un’operazione di salvataggio di un documento preveda l’aggiornamento di un’entità che raccoglie statistiche aggregate con un suo repository dedicato, abbiamo uno scenario tipico in cui si vuole che tali operazioni siano fatte in maniera transazionale. Il concetto centrale è che la transazionalità della persistenza dei dati è una responsabilità del layer dei dati, ma la scelta di cosa debba avvenire in maniera transazionale e cosa non entra pienamente nel campo del dominio dell’applicazione. In generale un qualsiasi pezzo di business logic transazionale agirà in questo modo: • Richiederà delle entità di dominio dallo strato di persistenza (quindi dai singoli repository). • Modificherà tali entità di dominio attraverso la collaborazione delle entità e di servizi di dominio specifici. 88 4 -­‐ L’architettura del sistema • Persisterà i risultati dell’operazione nel database. La transazionalità in questo caso dovrà essere garantita per tutta l’operazione. Se qualcosa va storto durante la persistenza di un’entità del dominio, dovrà essere possibile fare un rollback su quelle in precedenza persistite. L’architettura progettata segue il seguente schema: Immagine 31: Request Response Layer Il client manda un oggetto di tipo request ad un service executor, il service executor ottiene il tipo di servizio adatto al tipo di richiesta. A questo punto 89 4 -­‐ L’architettura del sistema viene richiesta un'istanza del servizio ad un motore di Inversion of Control che si occuperà di istanziare tutte le dipendenze del caso (una serie di repository e data context con la politica adatta transiente o singleton). A questo punto viene richiamata la execute del servizio, in cui viene aperto il data context, vengono fatte le varie richieste ai repository (che come abbiamo visto diventeranno query vere e proprie al database). Ottenute tutte le entità dallo strato di persistenza, viene applicata la business logic con gli oggetti in memoria che successivamente possono essere persistiti. La logica alla base dell’architettura è che all'inteno del servizio (che può essere servito su un thread separato) è utilizzato lo stesso datacontext, incapsulando idealmente tutto il servizio all’interno di quello che nell’immagine viene chiamato “transaction scope47”. Alla fine delle operazioni viene assemblata una risposta opportuna che viene restituita al modello della vista che l’ha invocata. 4.3.5 Presentation layer Il presentation rappresenta l’ultimo strato del fat client. Anche in questo caso l’ottica di scrivere codice riusabile su altre piattaforme ha posto di fronte a delle sfide architetturali: • Necessità di scrivere codice specifico per iPad utilizzando le librerie grafiche esposte dalla piattaforma Apple. • Volontà di mantenere la logica delle viste indipendente dalla piattafroma. L’approccio utilizzato è stato quello dell’utilizzo di una declinazione dell’MVP 48 con passive view. 47
Transaction Scope = Una connessione + Una transazione 48
Model View Presenter 90 4 -­‐ L’architettura del sistema View Presenter Model Immagine 32: Model View Presenter Come possiamo vedere dal diagramma: • La view conosce l’istanza del presenter. • Il presenter è l’unica classe che sa come raggiungere il modello e interrogarlo per ottenere i dati da mostrare. • Il presenter parla con la view attraverso una sua interfaccia (astrazione dell’UI con nessun riferimento al codice specifico della piattaforma su cui la view è implementata). • La View non conosce assolutamente il modello. Con questo schema architetturale si è ottenuto un presenter che contiene solo la logica di scambio d’informazioni con la vista, e parlando con un’interfaccia non ne conosce i dettagli implementativi. Sarebbe stato possibile introddurre un ulteriore livello di indirezione per la comunicazione della vista con il presenter esponendolo come interfaccia. La cosa sarebbe stata inutile poichè si ha la certezza di poter riutilizzare completamente il 91 4 -­‐ L’architettura del sistema codice del presenter su tutte le potenziali piattaforme target poiché a livello implementativo utilizza solamente entità POCO49. Immagine 33: Vista di creazione e modifica di un'ordine. L’approccio si è rivelato valido e ci ha permesso di fornire un “look and feel” nativo all’applicazione senza sacrificarne però la portabilità. L’unico codice platform specific è quello che è necessario che lo sia ed è nascosto dietro all’interfaccia della view. 49
Plain Old CLR Object 92 5 -­‐ La qualità del software 5 La qualità del software Una parte molto importante del lavoro che è stato fatto durante tutto il progetto riguardala qualità del software. In un progetto enterprise, con un supporto lungo, la qualità è legata principalmente a due aspetti fondamentali: • Correttezza del software La parte fondamentale della qualità del software riguarda la correttezza. Questa caratteristica può essere ottenuta attraverso metodologie di testing che saranno analizzate. • Manutenibilità La manutenibilità riguarda la facilità con cui si riescono a correggere difetti nel software e quella con cui si riescono ad implementare nuove caratteristiche senza che la parte già sviluppata subisca regressioni. In questo caso si vedrà come l’applicazione delle metodologie di testing contribuisca alla manutenibilità e saranno analizzate alcune buone prariche per quanto riguarda la gestione del progetto. 5.1 Il Testing La fase di testing consiste nel verificare la coerenza del software sviluppato con i requisiti iniziali. I requisiti si possono presentare in molteplici forme, che spaziano da specifiche informali (espresse dall’utente finale), a quelle formalizzate di un’analista. Nel primo caso è spesso difficile produrre dei test adeguati proprio per l’incompletezza del requisito. Nel caso di specifiche ben formalizzate è invece molto più semplice adottare tecniche e metodologie per la costruzione di test adeguati. Si può quindi affermare che il primo passo per la stesura di procedure di collaudo adeguato, non rappresenta un’attività strettamente tecnica, quanto piuttosto un processo di comprensione e formalizzazione di requisiti destrutturati e incompleti. 93 5 -­‐ La qualità del software Essendo questa parte profondamente dipendente dal contesto del progetto, e difficilmente standardizzabile in questa trattazione affronteremo soltanto le metodologie tecniche con cui collaudare un software a partire da specifiche ben formalizzate. Nello specifico si vedrà come sono state affrontate le seguenti tipologie di test: • Test unitari • Test d’integrazione • Test di accettazione 5.1.1 I test unitari I test unitari rappresentano il test sulla più piccola unità testabile di un progetto software. Nello specifico caso della programmazione ad oggetti l’unità testata è spesso una classe o un’interfaccia. A livello ideale ogni unit test deve essere isolato e indipendente dagli altri. Tutte le dipendenze che servono a un oggetto sotto unit test non devono essere vere ma simulato attraverso l’utilizzo dei cosiddetti “mock” o “stub”. In questo modo si assicura il fatto che se c’è un errore questo è effettivamente localizzato nel componente sotto test e non di un componente esterno e fuori dal controllo. I test unitari possono essere scritti in diversi modi. L’utilità degli unit test viene esaltata dall’utilizzo di framework per la scrittura di test automatici. Una souite di test automatici permette di verificare periodicamente lo stato della correttezza del software durante tutto il ciclo di sviluppo e funziona come vera e propria “rete di salvataggio” per le regressioni nel caso in cui si voglia modificare l’implementazione di qualche dettaglio. 94 5 -­‐ La qualità del software I test unitari sono stati utilizzati in maniera estesa in tutto il progetto garantendo una copertura estremamente elevata della base di codice. Per implementare i test è stato utilizzato Nunit, un framework open source nato come porting del famoso Junit nel mondo .NET e divenuto presto il punto di riferimento nel suo campo. Per la creazione di oggetti mock e stubs negli gli unit test ci si è avvalsi del framework Rhino Mocks. Per capire cosa significa scrivere un test unitario che coinvolge l’utilizzo di dipendenze “mockate”, si può far riferimento ad uno dei test implementati per verificare la correttezza di un motore di template scritto per il generatore dei numeri dei documenti all’interno del progetto. Il test è scritto come metodo di una classe di test con un opportuno attributo per indicare al runner che deve essere eseguito. Il nome del metodo rappresenta cosa si vuole verificare con il test. 1. [Test] 2. public void CanGenerateDocumentNumberUsingTemplate() 3.
{
Snippet 20: Firma di un metodo di test del generatore di numeri di documenti. In questo caso si considera un test che afferma che è possibile generare un numero di documento utilizzando un template. All’interno del metodo di test è utilizzata la cosiddetta strategia AAA per strutturare il test chesi compone di tre fasi: • Arrange Consiste nella fase di generazione dei mock, di istanziazione di specifici oggetti. Si tratta in pratica della “configurazione” del nostro ambiente di test. 95 5 -­‐ La qualità del software • Act Consiste nello scatenare il metodo sotto test. • Assert Consiste nel fare delle asserzioni sul risultato dello step precedente, che siano su un valore restituito o sullo stato del sistema. Possiamo vedere com’è composta la fase di Arrange nel test preso in considerazione: 1. // Arrange 2. var valuesRepository = MockRepository 3. .GenerateStrictMock<IDocumentNumbersValueRepository>(); 4. 5. var documentType = new DocumentType 6. { 7. Id = "ORD", 8. DocumentFamilyId = DocumentFamilyId.SalesOrder 9. }; 10. 11. var numberRegistry = new NumbersRegistry 12. { 13. Id = "SalesOrderRegistry", 14. NumberPrefix = "ORDI", 15. }; 16. var documentNumbersRegistry = new DocumentNumbersRegistry 17. { 18. NumbersRegistryId = numberRegistry.Id, 19. DocumentFamilyId = DocumentFamilyId.SalesOrder, 20. }; 21. var salesOrderRegistryValueFor2011 = new DocumentNumbersValue 22. { 23. NumbersRegistryId = numberRegistry.Id, 24. Year = 2011, 25. Value = 24 26. }; 27. 28. valuesRepository 29. .Expect(x => x.FindBy(numberRegistry.Id, 2011)) 30. .Return(salesOrderRegistryValueFor2011); 31. 32. 96 5 -­‐ La qualità del software 33. valuesRepository 34. .Expect(x => x.Save(Arg<DocumentNumbersValue> 35. .Matches(y => 36. (y.NumbersRegistryId == numberRegistry.Id) 37. && (y.Year == 2011) 38. && (y.Value == 25) 39. ))); 40. 41. var templateService = MockRepository. 42. GenerateStub<IDocumentNumbersTemplateService>(); 43. 44. templateService 45. .Expect(x => x.GetTemplate(documentType)) 46. .Return("${RegistryPrefix}${Number:00000}${UserSeries}"); Snippet 21: Fase di Arrange di un test unitario. Si può osservare come in successione sono fatte le seguenti azioni: • È generato un mock per il repository dei progressivi dei documenti. • È istanziato un Registro numeratori opportunamente valorizzato. • È istanziato un Registro numeratore documenti. • È istanziato un progressivo che rappresenta il valore attuale del numeratore di documenti per lo specifico id. • È initettato il comportamento del repository in modo che risponda con il valore appena istanziato per una ricerca rispetto all’anno 2011 e a un determinato id di registro. • È configurato il motore di mock in modo da verificare che sia chiamata la save sul repository dei progressivi con l’anno 2011 e il numero incrementato di un’unità rispetto a quello estratto. (questa può essere vista come una configurazione della fase di Assert che il framework in qualche modo nasconde). • È generato un mock per il servizio di template. • È iniettato un comportamento che restituisce un template definito alla chiamata del metodo GetTemplate. 97 5 -­‐ La qualità del software A seguire la fase di Act scatena il sistema sotto test in questa maniera. 1. // Act 2. const string userSeries = "/US"; 3. var generatorParams = new DocumentNumberGeneratorParams( 4. documentType, 5. numberRegistry, 6. documentNumbersRegistry, 7. new DateTime(2011, 11, 16), 8. userSeries); 9. var generator = new DocumentNumberGenerator( 10. valuesRepository, 11. templateService); 12. var number = generator.Generate(generatorParams); Snippet 22: Fase di Act di un test unitario. Per fare delle asserzioni finali sul risultato ottenuto: 1. // Assert 2. Assert.IsNotNullOrEmpty(number); 3. Assert.AreEqual("ORDI00025/US", number); Snippet 23: Fase di Assert di un test unitario. Com’è facile vedere si è riusciti a testare un componente in maniera unitaria che ha delle dipendenze esterne, disaccoppiando però il test dalla correttezza del comportamento delle diendenze attraverso l’uso di mocks. Immagine 34: Test runner in visual studio 2010 I test unitari sono stati inseriti tutti in un progetto in modo da rendere immediato il lancio dell’intera souite. 98 5 -­‐ La qualità del software 5.1.2 I test unitari come strumento di design I test unitari non sono solo uno strumento per la verifica della correttezza di un algoritmo specifico ma possono essere utilizzati a supporto del processo di design di componenti software. La pratica che permette di ottenere questo effetto si chiama TDD50 e ribalta la visione classica dell’ingegneria del software per quanto riguarda il testing. La scrittura dei test unitari diventa il punto iniziale dello sviluppo, prima che ci sia qualcosa effettivamente da testare. La metodologia si basa su cicli iterativi molto brevi che seguono il seguente flusso: 50 Test Driven Development 99 5 -­‐ La qualità del software • Red Il rosso è associato ai vari framework di test che storicamente restituiscono in output una barra rossa quando un test unitario fallisce. Il primo step deve essere la scrittura di un test che fallisce. • Green Subito dopo scritto un test che fallisce, bisogna far diventare la barra verde, scrivendo codice d’implementazione che faccia passare il test appena scritto. In questa fase non va cercato il design perfetto ma solo il necessario a far passare il test. • Refactor Una volta fatto passare il test si utilizzano tecniche di refactoring e principi d’ingegneria del software per migliorare il codice scritto, avendo come ancora di salvezza la rete di test lasciata in eredità dalle prime due fasi. Tale processo incrementale prosegue con la scrittura successiva di test unitari, l’implementazione di una soluzione veloce e il refactoring. Sebbene la pratica possa sembrare strana a un primo sguardo, la potenza di un approccio di questo tipo ha motivazioni solide e profonde: • Il ciclo di feedback rapido aumenta la conoscenza riguardo all’adeguatezza dell’architettura implementata riapetto al problema specifico affrontato. • Scrivere test automatici prima della soluzione aiuta a concentrarsi su “cosa si vuole risolvere” senza perdersi nei dettagli implementativi. In questo caso si tratta di una vera e propria fase di analisi che però 100 5 -­‐ La qualità del software produce delle “specifiche” direttamente eseguibili sotto forma di test unitari. • Fare design con i test lascia in eredità una rete di test che permette di verificare in ogni momento se ci sono regressioni all’interno della base di codice. • La souite di test, che è un sottoprodotto di questa pratica, integra documentazione scritta, inquanto esplicita in maniera diretta le intenzioni dello sviluppatore nell’implementazione dello specifico componente. • Scrivere i test prima aumenta la qualità del codice. Un codice disaccoppiato è per sua natura testabile. Se è facile scrivere codice non testabile, è invece difficile farlo se si è partiti dalla stesura di un test. Ovviamente tale pratica è utilissima come integrazione alla classica analisi dell’ingegneria del software come ulteriore strato di validazione più vicino all’implementazione vera e propria. Una corretta analisi di dominio preliminare e la stesura di documentazione non possono in alcun modo essere sostituite dalla pratica in considerazione poiché affrontano problemi altrettanto importanti e sostanzialmente differenti. 5.1.3 I test d’integrazione I test d’integrazione differiscono da quelli unitari inquanto vanno a testare la collaborazione di componenti software anche molto disparati. Il test d’integrazione rappresenta l'estensione logica dello unit test. Il tipo più semplice di test d’integrazione consiste nella combinazione di due unità già sottoposte a test in un solo componente. In questo caso, per componente si intende l'aggregazione integrata di più unità. In uno scenario realistico più 101 5 -­‐ La qualità del software unità sono combinate in componenti che, a loro volta, vengono aggregati in parti più grandi del programma. Il concetto che è alla base di quest’approccio consiste nell'esecuzione del test delle combinazioni di parti. Il test d’integrazione consente di individuare i problemi che si verificano quando due unità si combinano. Se si utilizza un piano di test che prevede il collaudo di ciascun’unità e la verifica della validità di ognuna prima di combinarle, sarà evidente che gli errori rilevati nella combinazione delle unità sono probabilmente legati al relativo interfacciamento. In generale nei test d’integrazione non si fa uso di mocks o stub ma si utilizza in tutto e per tutto codice d’implementazione. La struttura di un test d’integrazione non differisce in maniera sostanzialo da quanto detto per i test unitari quindi non sarà rifatta un’analisi approfondita. Quello che vale la pena evidenziare è la modalità con cui testare a livello di integrazione componenti che accedono al database. Il problema dei test che accedono sul database è dovuto al fatto che l’ambiente di test per essere corretto deve essere sempre lo stesso per ciascun’esecuzione del test. In generale se supponiamo l’esistenza di una batteria di test che accede al database, possibilmente vi apporta modifiche e fa asserzioni su esso il comportamento voluto deve seguire il seguente flusso logico per ogni test: Inserimento di un dataset iniziale per lo scenario di test Esecuzione del test con persistenza sul database Asserzioni sullo stato utilizzando dati estratti dal database Pulizia del database Immagine 35: Processo per i test d’integrazione sul database. 102 5 -­‐ La qualità del software Com’è facile capire la scrittura manuale di test che rispettino questo processo è un processo abbastanza laborioso. A livello implementativo si tratta di scrivere una query d’inserimento del dataset iniziale, il vero e proprio codice di test, e una query di pulizia del dataset. La scrittura di query manuali è un processo abbastanza error prone e non ci sono strumenti di supporto per rendere veloce la cosa. Per superare questo problema è stato messo in campo l’utilizzo di un apposito framework chiamato NdbUnit. Quello che il framework permette di fare è l’automatizzazione del processo di caricamento del dataset di test e la sua pulizia attraverso la stesura di due file di definizione in XML: • TestsSchema • TestsData Si può vedere un esempio di com’è implementato il test d’integrazione del repository dei metodi di pagamento. Come prima cosa va definito lo schema delle tabelle che saranno toccate dal test: 1. <?xml version="1.0" encoding="utf-­‐8"?> 2. <xs:schema id="PaymentMethodTestSchema" 3. targetNamespace="http://tempuri.org/PaymentMethodTestSchema.xsd" 4. xmlns:mstns="http://tempuri.org/PaymentMethodTestSchema.xsd" 5. xmlns="http://tempuri.org/PaymentMethodTestSchema.xsd" 6. xmlns:xs="http://www.w3.org/2001/XMLSchema" 7. elementFormDefault="qualified"> 8. <xs:element name="TestsData" > 9. <xs:complexType> 10. <xs:choice minOccurs="0" maxOccurs="unbounded"> 11. <xs:element name="PaymentMethod"> 12. <xs:complexType> 13. <xs:sequence /> 14. <xs:attribute name="Id" type="xs:string" use="required" /> 15. <xs:attribute name="Description" type="xs:string" /> 16. <xs:attribute name="Discount1" type="xs:decimal" use="required"/> 17. <xs:attribute name="Discount2" type="xs:decimal" use="required"/> 103 5 -­‐ La qualità del software 18. <xs:attribute name="Discount3" type="xs:decimal" use="required"/> 19. </xs:complexType> 20. </xs:element> 21. </xs:choice> 22. </xs:complexType> 23. </xs:element> 24. </xs:schema> Snippet 24 : Schema per il test di integrazione per il repository dei metodi di pagamento. In seguito va definito il dataset che si può considerare lo stato di partenza di ogni singolo test: 1. <TestsData xmlns="http://tempuri.org/PaymentMethodTestSchema.xsd" > 2. <PaymentMethod 3. Id="TestPM" 4. Description="Pagamento di Test" 5. Discount1="10" 6. Discount2="50" 7. Discount3="20"/> 8. <PaymentMethod 9. Id="100" 10. Description="Pagamento alla consegna" 11. Discount1="0" 12. Discount2="0" 13. Discount3="0"/> 14. <PaymentMethod 15. Id="101" 16. Description="Finanziamento" 17. Discount1="0" 18. Discount2="0" 19. Discount3="0"/> 20. <PaymentMethod 21. Id="R.D.30" 22. Description="Rimessa Diretta 30GG DF SC 2,5%" 23. Discount1="2.5" 24. Discount2="0" 25. Discount3="0"/> 26. </TestsData> Snippet 25: Dataset iniziale per i test di integrazione del repository dei metodi di pagamento. 104 5 -­‐ La qualità del software A questo punto si può scrivere il test d’integrazione sapendo che il framework si occuperà in maniera autonoma del ripristino dello stato iniziale del db durante l’esecusione di ogni test: 1. public abstract class PaymentMethodBaseRepositoryTests : BaseRepositoryTests 2. { 3. [Test] 4. public void CanReadAllData() 5. { 6. var repository = CreateRepository(); 7. var expected = CreateExpectedTestPaymentMethod(); 8. var actual = repository.GetById(expected.Id); 9. AssertPaymentMethodsAreEqual(expected, actual); 10. } 11. 12. [TestCase("100", "Pagamento alla consegna")] 13. [TestCase("R.D.30", "Rimessa Diretta 30GG DF SC 2,5%")] 14. [TestCase("R.D.60", "Rimessa Diretta 60GG DF SC 2%")] 15. public virtual void CanGetById(string id, string expectedDescription) 16. { 17. var repository = CreateRepository(); 18. var actual = repository.GetById(id); 19. Assert.IsNotNull(actual); 20. Assert.AreEqual(id, actual.Id); 21. Assert.AreEqual(expectedDescription, actual.Description); 22. } 23. 24. [TestCase("pagam consegna terms", "SBT-­‐PC")] 25. [TestCase("diretta rimessa terms", "SBT-­‐RD30,SBT-­‐RD60,SBT-­‐RD090")] 26. [TestCase("terms SBT-­‐RD", "SBT-­‐RD30,SBT-­‐RD60,SBT-­‐RD090")] 27. [TestCase("terms rd 90", "SBT-­‐RD090")] 28. public virtual void CanSearchByTerms(string terms, string expectedIds) 29. { 30. var repository = CreateRepository(); 31. var actual = repository.SearchByTerms(terms, 0, 10); 32. Assert.IsNotNull(actual); 33. var actualIds = string.Join(",", actual.Select(x => x.Id).ToArray()); 34. Assert.AreEqual(expectedIds, actualIds); 35. } 36. } Snippet 26: Test di integrazione del repository dei pagamenti. 105 5 -­‐ La qualità del software L’utilizzo di tale tecnica ha permesso di avere test d’integrazione per tutti i repository implementati, sia su database Sql Server sia su SQLITE. Immagine 36: Radice della directory dei test di integrazione per i repository. 106 5 -­‐ La qualità del software 5.1.4 I test di accettazione Con test di accettazione comunemente s’intendono i test che verificano direttamente l’implementazione di una feature richiesta del cliente. I test di accettazione possono presentarsi in diverse forme e in generale non sono automatizzabile in maniera semplice. In alcuni casi è però possibile avere una formalizzazione dei requisiti tale da permettere l’implementazione di test di accettazione automatizzati. Come detto in precedenza la soluzione progettata ha un fat client che deve poter lavorare offline; questo significa che la parte di logica dell’ERP che si occupa di calcolare i prezzi va reimplementata per ogni azienda che lo richiede. Questa parte di logica di business è quella centrale del client; quella che determina la correttezza della creazione di un ordine sul client mobile. L’importanza di tale parte del sistema ha reso necessaria l’esplorazione di una strategia per scrivere velocemente test di accettazione. In generale i test di accettazione per quanto riguarda il calcolo dei prezzi sono forniti come dati tabellari. È dato un dataset di riferimento, delle condizioni di calcolo e il risultato ottenuto. Quello che è stato fatto è stato implementare attraverso l’utilizzo di un framework BDD51 un binding tra test di accettazione scritti in linguaggio naturale e le classi di dominio, per rendere direttamente eseguibili i test. Nello specifico è stato utilizzato il framework SpecFlow che ha semplificato in maniera sostanziale il compito di parsing delle specifiche in linguaggio naturale e il binding con le entità di dominio. Un test di Immagine 37: Logo SpecFlow accettazione in stile BDD può essere formulato esplicitando le seguenti fasi: 51 Behaviour Driven Development 107 5 -­‐ La qualità del software • Scenario Nello scenario è data una descrizione dello scenario che si sta testando. • Dato Questa parte coincide con l’Arrange del test unitario visto in precedenza. In questa fase sono impostati tutti i parametri per cui si vuole far girare il test. • Quando Questa parte coincide con l’Act del test unitario visto in precedenza. Nel quando si specifica l’azione che si intende fare. • Allora Questa parte coincide con l’Assert del test unitario visto in precedenza. Si verifica che il risultato dell’operazione sia conforme a quanto specificato. Si può vedere un’implementazione di un test di accettazione eseguibile automaticamente scritto in linguaggio naturale: Scenario: Calcolo Prezzo per Condizione di Vendita 01 (Cliente + Articolo), 09 (Produttore + Cat. Sconto Vendita) Dato il cliente '1000008’ E l'articolo '00052245’ E la data di riferimento '10 Set 2007’ Quando calcolo il prezzo Allora il prezzo dovrebbe essere 36,760026 E lo sconto dovrebbe essere '52 + 10' Snippet 27: Test di accettazione eseguibile scritto in linguaggio naturale Per ottenere un test eseguibile di questo tipo in linguaggio naturale l’unica cosa da fare è scrivere una classe che esplicita i binding: 108 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
5 -­‐ La qualità del software [Binding, StepScope(Tag = "PriceCalculator")] public class PriceCalculatorTestsStepDefinitions { private readonly PriceCalculatorTests.TestInputs _inputs = new PriceCalculatorTests.TestInputs(); private Price _calculatedPrice; [Given("il cliente '(.*)'")] public void GivenTheCustomer(string customerId) { _inputs.CustomerId = customerId; } [Given("l'articolo '(.*)'")] public void GivenTheItem(string itemId) { _inputs.ItemId = itemId; } [Given("la data di riferimento '(.*)'")] public void GivenTheItem(DateTime date) { _inputs.Date = date; } [When("calcolo il prezzo")] public void WhenThePriceIsCalculated() { var mockFactory = CreateMockFactory(); _calculatedPrice = PriceCalculatorTests.CalculatePrice( _inputs, mockFactory); } [Then("il prezzo dovrebbe essere (.*)")] public void ThenThePriceShouldBe(decimal expectedPrice) { Assert.AreEqual( expectedPrice, _calculatedPrice.UnitPrice); } [Then("lo sconto dovrebbe essere '(.*)'")] public void ThenTheDiscountsShouldBe(string expectedDiscounts) { string formattedDiscounts = ValueFormatter.FormatDiscounts(_calculatedPrice.Discounts); Assert.AreEqual(expectedDiscounts, formattedDiscounts); } private IPriceCalculatorMockFactory CreateMockFactory() { var tags = FeatureContext.Current.FeatureInfo.Tags; if (tags == null) throw new Exception("Tags not specified for feature!"); if (tags.Contains("EnterpriseA2007")) return new EnterpriseA2007MockFactory(); if (tags.Contains("EnterpriseA2011")) return new EnterpriseA2011MockFactory(); if (tags.Contains("EnterpriseB2012")) return new EnterpriseB2012MockFactory(); } } Snippet 28: Binding tra linguaggio naturale e classi di dominio per test con specflow. 109 5 -­‐ La qualità del software L’utilizzo di questa tecnica ha permesso di poter richiedere delle specifiche formalizzate direttamente eseguibili anche a personale non direttamente competente nell’ambito della programmazione. La stesura, infatti, non richiede particolari capacità tecniche e può essere fatta da un qualsiasi esperto di dominio senza passare per il reparto tecnico. 5.2 Metriche Le scelte architetturali, le metodologie, il testing sono tutti strumenti che sono stati utilizzati al fine di produrre codice di qualità che possa essere mantenuto nel tempo. In quest’ottica vale la pena analizzare alcune metriche ricavate dalla base di codice prodotta. Nella fattispecie si vedranno quelle fornite dal tool d’analisi del codice fornito con Visual Studio 2010. 5.2.1 Complesità Ciclomatica La complessità ciclomatica di una sezione di codice è rappresenta il numero di cammini linearmente indipendenti attraverso il codice sorgente. La definizione di "cammini linearmente indipendenti" risiede nella definizione di lineare indipendenza a livello algebrico, semplicemente estesa ai cammini su grafi orientati. Una complessità ciclomatica di 4, significa che 4 è il numero massimo di cammini possibili tra loro indipendenti e ogni altro cammino possibile sul grafo si può costruire a partire da uno di quei 4 (vale a dire una combinazione lineare di uno di essi). In termini meno formali la complessità ciclomatica rappresenta il numero di decisioni che possono essere prese all’interno di una procedura. I tool calcolano tale metrica analizzando la presenza delle seguenti parole chiave: • if • while • for • foreach • case 110 5 -­‐ La qualità del software • default • continue • goto • && • || • catch • ?: (operatore ternario) • ?? (operatore per il controllo dei null) I metodi con complessità ciclomatica superiore a quindici sono considerati generalmente difficili da comprendere, quelli con complessità ciclomatica maggiore di 30 sono estremamente complessi e devono essere divisi in sottometodi. 5.2.2 Profondità dell’ereditarietà La profondità dell’ereditarietà indica il numero di definizioni di classi che servono per arrivare alla radice della gerarchia delle classi a partire da quella in analisi. Maggiore è la profondità e maggiore è la difficolta nel capire dove particolari metodi e proprietà sono stati definiti o ridefiniti. In generale classi con profondità di ereditarietà maggiore a sei sono da considerarsi difficili da mantenere. Non è una regola assoluta poiché esistono molti esempi di classi di framework solidi e diffusiche hanno un livello di ereditarietà mendia intorno a cinque e sono comunque molto manutenibili. 5.2.3 Accoppiamento tra classi Misura l’accoppiamento tra classi attraverso l’analisi di parametri d’ingresso, variabili locali, tipi di ritorno, chiamate a metodi, istanziazione di tipi generici, classi base, implementazione d’interfaccie. Un buon design del software deve portare a metodi che hanno alta coesione e basso accoppiamento. Un alto accoppiamento spesso è fonte di estrema difficoltà nella modifica e nella manutenzione delle classi a causa delle molte interdipendenze. Classi con alto 111 5 -­‐ La qualità del software accoppiamento solitamente sono anche difficili da riusare. Il tool di metriche calcola l’accoppiamento con la combinazione di due parametri: • Efferent Coupling: Calcolato come il numero di tipi all’interno del package che dipendono da tipi al di fuori del package. • Afferent Coupling: Numero di tipi esterni al package che dipendono dai tipi all’interno del package sotto analisi. 5.2.4 Indice di manutenibilità L’indice di manutenibilità rappresenta un’aggregato di alcune delle precedenti metriche precedente metriche che segue la seguente formula: 𝑀𝐼 = max 0,171 − 5.2 ∗ ln 𝐻𝑉 − 0.23 ∗ 𝐶𝐶 − 16.2 ∗ ln 𝐿𝑜𝐶
∗ 100/171) Dove: 𝑀𝐼 = 𝑀𝑎𝑖𝑛𝑡𝑎𝑖𝑛𝑎𝑏𝑖𝑙𝑖𝑡𝑦 𝐼𝑛𝑑𝑒𝑥 𝐻𝑉 = 𝐻𝑎𝑙𝑠𝑡𝑒𝑎𝑑 𝑉𝑜𝑙𝑢𝑚𝑒 = 𝑁 ∗ 𝑙𝑜𝑔 log ! 𝜂 𝜂 = 𝑁𝑢𝑚𝑒𝑟𝑜 𝑑𝑖 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟𝑖 𝑑𝑖𝑠𝑡𝑖𝑛𝑡𝑖 + 𝑁𝑢𝑚𝑒𝑟𝑜 𝑜𝑝𝑒𝑟𝑎𝑛𝑑𝑖 𝑑𝑖𝑠𝑡𝑖𝑛𝑡𝑖 𝑁 = 𝑁𝑢𝑚𝑒𝑟𝑜 𝑡𝑜𝑡𝑎𝑙𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟𝑖 + 𝑁𝑢𝑚𝑒𝑟𝑜 𝑡𝑜𝑡𝑎𝑙𝑒 𝑂𝑝𝑒𝑟𝑎𝑛𝑑𝑖 𝐶𝐶 = 𝐶𝑜𝑚𝑝𝑙𝑒𝑠𝑠𝑖𝑡à 𝐶𝑖𝑐𝑙𝑜𝑚𝑎𝑡𝑖𝑐𝑎 𝐿𝑜𝐶 = 𝐿𝑖𝑛𝑒𝑒 𝐷𝑖 𝐶𝑜𝑑𝑖𝑐𝑒 Dall’equazione è semplice vedere come i vari indici vadano a scalare la manutenibilità su un indice che va da zero a cento. Più vicino a cento è l’indice migliore è la manutenibilità del software. La microsoft propone le seguenti soglie per quanto riguarda questo indice: • 0-­‐9 È marcato in rosso; si tratta di codice molto difficile da mantenere. Si rende necessario un refactoring profondo del codice. • 10-­‐19 112 5 -­‐ La qualità del software È marcato in giallo; singifica che il codice potrebbe avere dei problemi di manutenibilità. Si consiglia di eseguire del refactoring per migliorare la situazione. • 20-­‐100 Il codice è molto manutenibile, non sono richieste operazioni di refactoring. Immagine 38: Risultati dell'analisi del codice di visual studio. Nel report generato dal tool utilizzato, possiamo vedere come le scelte progettuali abbiano portato a un’alta manutenibilità delle varie componenti del software. 113 6 -­‐ Conclusioni e sviluppi futuri 6 Conclusioni e sviluppi futuri Con questa trattazione si sono viste tutte le varie fasi che hanno guidato la progettazione e lo sviluppo di un software complesso di classe enterprise. In particolare si sono elencate le scelte strutturali che hanno reso possibile lo sviluppo completo della parte centrale del software. Si è inoltre verificato attraverso l’utilizzo di strumenti di analisi statica come le tecniche messe in atto e le scelte effettuate hanno prodotto una base di codice di qualità, pronta per essere estesa negli anni a venire. Gli obiettivi fissati in fase di organizzazione del progetto sono stati conseguiti, e una versione preliminare e funzionante del software è già stata installata presso due clienti importanti con agenti selezionati che stanno utilizzando la soluzione. In una visione di medio termine il progetto ha di fronte una lunga serie di nuove funzionalità da implementare che sono già state programmate. Per cercare di stringere i tempi per andare sul mercato, senza che questo influisse sulla qualità del prodotto, si è deciso sin dall’inizio di puntare sullo sviluppo di un client iPad. Come evidenziato, si è fatto in modo che la scelta non fosse in alcun modo vincolante dal punto di vista dei possibili sviluppi futuri; in una visione di lungo termine saranno sicuramente sviluppati client per piattaforma Android e Windows Mobile /Windows 8. 114 7 -­‐ Bibliografia 7 Bibliografia • xUnit Test Patterns (Refactoring Test Code) Gerard Meszaros • Test Driven Development: By Example Kent Beck • Comparing Java and .NET Security: Lessons Learned and Missed Nathanael Paul David Evans •
Patterns of Enterprise Application Architecture Martin Fowler •
Enterprise Integration Patterns Gregor Hohpe •
Integration Patterns David Trowbridge Ulrich Roxburgh Gregor Hohpe Dragos Manolescu E.G. Nadhan •
Microsoft .NET: Architecting Applications for the Enterprise Dino Esposito Andrea Saltarello •
C# in Depth Jon Skeet •
Dependency Injection In .NET Mark Seemann •
Design Patterns: Elements of Reusable Object-­‐Oriented Software Erich Gamma Richard Helm Ralph Johnson John Vlissides 115 7 -­‐ Bibliografia •
Common Language Infrastructure (Standard ECMA-­‐335) •
Architectural Styles and the Design of Network-­‐based Software Architectures Fielding Roy Thomas •
RFC 2616: Hypertext Transfer Protocol (HTTP/1.1) •
RESTful Web Services -­‐Web services for the real world Leonard Richardson Sam Ruby •
Principi di Ingegneria del Software R. Pressman 116 Ringraziamenti Con queste ultime righe, desidero fare alcuni ringraziamenti: Desidero ringraziare la mia famiglia che mi ha supportato e sopportato durante tutto il mio percorso di studi. Desidero ringraziare il professor Aldo Franco Dragoni che è stato sempre disponibile durante il mio percorso accademico, diventando di fatto un punto di riferimento fondamentale per la mia esperienza universitaria. Ringrazio e-­‐xtrategy e kilog per aver creduto nelle mie capacità dandomi la possibilità di esprimere le mie competenze su un progetto importante come quello affrontato. Nello specifico ringrazio il mio tutor aziendale Lorenzo Massacci per essere stato sempre disponibile durante tutto il periodo del tirocinio, sia tecnicamente sia umanamente. Ringrazio Stefano e Gabriele Ottaviani per le ore passate a progettare, pianificare e sviluppare insieme un progetto partito con un editor di testo vuoto e arrivato in pochi mesi a essere un prodotto funzionante. Ringrazio inoltre Gianluigi, compagno di studi e amico, con cui ho condiviso le fatiche di esami e progetti. L’ultimo ringraziamento, ma non per importanza, va a tutti i miei amici, che hanno contribuito a rendere piacevoli tutti i momenti passati insieme degli ultimi anni. 117