Un mondo di servizi che devono essere utili…
Chiariamo subito una cosa, nel contesto di questo post, servizi = WCF, quindi applicazione con un numero di tier >= 3.
Qualche “assioma” che esploreremo nel post:
- i servizi si usano se servono non perché fa figo;
- l’atomicità deve essere garantita sempre, magari non si può parlare di transazioni, ma l’atomicità (anche apparente) delle operazioni ci deve essere sempre;
- ogni volta che attraversate un confine c’è un costo notevole valutate bene cosa portate con voi;
- cercare di essere vagamente generici (ad esempio esponendo in maniera banale la CRUD) non serve a un ca*zo (scusate il francesismo);
Un’applicazione ha senso che sia n-tier se c’è almeno un requisito che lo richiede, e che sia un requisito di quelli seri , altrimenti i costi e la complessità che ci portiamo a casa sono talmente alti che il gioco quasi certamente non vale la candela; vediamone alcuni:
- security: una policy stabilisce che il front-end in nessun modo deve poter accedere direttamente alla base dati e quindi un application-tier è dovuto;
- più di un’applicazione fruisce dei nostri dati e abbiamo la necessità di poter rimodellare i dati a piacimento senza “spaccare” le applicazioni;
- scalabilità: abbiamo bisogno di poter scalare orizzontalmente, e tipicamente (ripeto tipicamente) è costoso e/o complesso far scalare orizzontalmente un database;
- Vogliamo aggregare fonti dati eterogenee al fine di renderle fruibili a diverse applicazioni senza spalmare su ogni applicazione la complessità dell’aggregazione;
- e chi più ne ha più ne metta…
Atomico (che fortunatamente non è Nucleare)
L’introduzione di un servizio porta con se una serie di problematiche che vanno valutate molto bene a fronte dei potenziali vantaggi, le problematiche infatti sono certe mentre i vantaggi non proprio. La prima problematica che dovete tenere in considerazione è l’impossibilità (o meglio l’insensatezza) di gestire le transazioni attraverso più tier. Perché? perché in generale le transazioni distribuite sono il “male”, all’interno del mio orticello la transazione è doverosa (è semplicemente stupido non usarle) ma una transazione su più orticelli è un problema bello grosso quindi meglio passare oltre.
Sta di fatto che l’atomicità (o presunta tale) dell’operazione la dobbiamo garantire, se non altro perché l’utente si aspetta che se ha uno screen in cui può creare un Employee, assegnarvi un Address e un Department e sullo screen c’è un singolo pulsante salva il nostro simpatico utente si aspetta che quell’operazione sia atomica, quindi se l’assegnazione dell’Address fallisce non è concepibile che l’Employee esista (punto-fine-della-discussione).
Quando avete a che fare con un servizio (o magari più servizi) questa cosa non è proprio così facile da realizzare, diciamo che avete le seguenti possibilità:
- esporre un’operazione che prenda in pasto tutte le entità e sappia come eseguire la cosa in maniera transazionale: il problema è che al crescere delle operazioni che l’utente deve poter fare la matrice delle combinazioni esplode rendendo di fatto non percorribile questa strada;
- esporre un’operazione per ogni singolo step giocando/introducendo il concetto di compensazione: il problema è che è tutto tranne che facile… vedo due strade possibili che potrebbero essere oggetto di altrettanti post;
- cambiare completamente approccio e introdurre al livello dei servizi i concetti di messaggistica e stato: sposate la complessità altrove ma sicuramente riducete la complessità/onerosità della manutenzione/gestione dei contratti dei servizi, o forse abbattate decisamente la complessità se cambiate punto di vista ;
…ed è sicuramente un subset ad hoc dell’armadio di casa, se siamo tutti d’accordo su questa affermazione la domanda che mi faccio è: perché vedo quasi sempre un modello di DTO che mima e scopiazza quasi al 100% il modello di dominio che sta dietro l’application-tier?
Se state partendo per una vacanza di mare preparate i bagagli pensando ad una vacanza stanziale, in un posto caldo, dove non credo proprio nevicherà (quindi è ipotizzabile che le condizioni climatiche siano stabili) mentre se state per avventurarvi in un mese in giro per l’Europa con l’InterRail probabilmente preparerete uno zaino super-comodo con l’essenziale e sarete pronti per condizioni climatiche molto variabili e impreviste. Quindi due scenari diversi e due set di bagagli completamente diversi, ma in tutti i casi provenienti dal vostro armadio.
Non solo…se partite per un viaggio non è pensabile (almeno nel 99%) dei casi tornare indietro a prendere qualcosa che ci siamo dimenticati, sapete è tipico arrivare a New York, non portare le ciabatte e tornare in Italia a prenderle e, peggio ancora, riportarle in Italia dopo averle usate… se tutti, come me, ritenete folle anche solo pensare una cosa del genere perché mai cercate di mettere Lazy-Load (o concetti similari) in un’applicazione multi-tier? magari pure un’applicazione web…dove il Lazy-Load è per definizione inutile?
Ogni volta che attraversate un confine (o partite per un viaggio) ha molto senso avere con se tutto quello che serve, modellato come serve, partendo dal presupposto che è molto costoso tornare indietro se ci si è dimenticati qualcosa.
Pensando ad un mondo SOA questa regola è dettata da uno dei 4 tenets: service boundary must be explicit, i confini di un servizio devono essere espliciti proprio perché devo sapere molto bene che sto attraversando un qualcosa che, in un certo senso, è senza ritorno.
Questa problematica è quella che più di tutte mi fa dire, peraltro da anni, che passare da un da un’applicazione n-layer ad un’applicazione n-tier in maniera indolore (o peggio ancora in maniera “nascosta” magari tramite IoC/DI) è impossibile se non un disastro annunciato.
Il concetto di bagaglio adeguato ci avvicina anche ad una cosa molto cara al mondo DDD (mondo che sto cercando di approcciare insieme a qualche pazzo di mia conoscenza) ci avvicina al concetto di aggregate root. Facciamo un esempio:
questo è un estratto dell’home page del sito di una notissima cantina vinicola siciliana, è importante notare che quella porzione della pagina è fatta in Flash, quindi a tutti gli effetti è come se fosse un’applicazione diversa dall’applicazione che ha renderizzato il resto della informazioni sulla pagina web. I dati ha quindi senso che siano esposti (e di conseguenza il modello protetto) da un servizio.
questo è invece un estratto della scheda prodotto, completamente in html, e i dati anche qui sono erogati dallo stesso servizio di cui sopra. Se notate alcuni dati sono presenti sia in home page che nella scheda del prodotto, abbiamo usato lo stesso modello per trasferire i dati? assolutamente no! due viaggi diversi, con destinazioni molto diverse. C’è un modello fatto ad hoc per quella porzione di home page in Flash e c’è un modello strutturalmente molto diverso pensato appositamente per la scheda prodotto. L’obiettivo è quello di andare una ed una sola volta, ad inizio viaggio, dai servizi, il nostro armadio, e preparare i bagagli come ci servono per quello specifico scenario.
Quello che abbiamo sono quindi n modelli ad oggetti ognuno pensato per un caso d’uso specifico:
- è costoso? sti*azzi (latino stavolta) se lo è;
- è importante farlo? a mio modo di vedere, se dovete (non se volete ma se dovete) avere un’applicazione n-tier, è vitale;
- a quali macro problemi andate incontro? principalmente tempi di sviluppo e manutenzione, oltre alla necessità (cosa che non do più così per scontata) che ci sia qualcuno che abbia analizzato e capito a fondo i casi d’uso;
- a quali macro vantaggi andate in contro? principalmente tempi di sviluppo e manutenzione, che sembra in netta contraddizione con quello che abbiamo appena detto ma in realtà non lo è, dipende sempre dal punto di vista e soprattutto dal fattore temporale, ergo che ciclo di vita la vostra applicazione avrà;
Soprattutto quando la generalizzazione è introdotta per risparmiare. Diciamo subito come la penso:
Una qualsiasi cosa di alta qualità difficilmente può essere a basso costo, se poi introduciamo strumenti per loro natura costosi (come i servizi) abbiamo la possibilità di portarci a casa la qualità (altissima), ma possibilità resta…mentre abbiamo la certezza di portarci a casa i costi; il vero effetto che nessuno prende mai in considerazione è che se introduciamo strumenti inutili, o li introduciamo male, non solo ci portiamo a casa costi alti certi e ma molto probabilmente produciamo qualcosa che non va come ci aspettiamo quindi più che qualità produciamo potenziali danni, che è molto peggio che produrre bassa qualità a basso costo. Se compero un paio di scarpe da 10€ avrò un’aspettativa di qualità proporzionata alla spesa, se compero una paio di scarpe da 300€ e non solo si rovinano alla svelta ma mi rovinano anche i piedi il minimo che può capitare è che faccio causa a chi le produce…Ora, in un mondo basato su servizi ha veramente molto senso, e probabilmente una parte di quello che sto per dire l’avete già dedotta, spaccare radicalmente in due le due famiglie di operazioni:
- lettura: la “catena” delle operazioni che portano il dato dalla sorgente dati a chi ne ha bisogno, ma soprattutto nella forma in cui ne ha bisogno deve essere il più corta possibile; più si allunga il percorso che il tutto deve fare più si tende a voler generalizzare per ridurre la mole di codice da manutenere e la generalizzazione porta con se inevitabilmente dei limiti che portano a sacrificare la qualità a favore dei costi, ma con i servizi di mezzo i costi sono già alti quindi sacrificare è solo controproducente; l’unica cosa che ha molto senso inserire nella pipeline di lettura è la security;
- scrittura: il processo di scrittura ha invece molto senso che segua una strada radicalmente diversa, magari addirittura con servizi diversi, pensati solo ed esclusivamente per la scrittura, con una pipeline di validazione, security, tracking etc. che può essere lunga a piacere; la cosa fondamentale nel processo di scrittura, soprattutto se ci sono di mezzo dei servizi, è cambiare punto di vista, non dobbiamo più pensare in termini di “salva questo grafo di oggetti” ma piuttosto dobbiamo pensare in termini di “porta il tuo (del servizio) grafo dallo stato in cui è a questo nuovo stato” (se avete pensato CQRS e Event Sourcing siete sulla mia stessa linea, io però sono ancora molto lontano dal capire a fondo e applicare, in particolare CQRS); è un po’ come se ragionassimo in termini di Snapshop e ChangeSet, abbiamo una situazione (uno Snapshot) e dato un set di modifiche (ChangeSet) vogliamo passare ad una nuova situazione nota;
Non vi spaventate e se qualcuno sta pensando “questo è pazzo” sappiate che vi capisco l’ho pensato anche io di me stesso fino al momento in cui ho scritto questo post che è frutto anche di una recentissima esperienza lavorativa (non solo quella dei vini siciliani) che è stata definitivamente illuminante.
Il problema di fondo è che da quando ho iniziato a ragionare in termini di servizi (pensando anche a SOA) mi sono sempre scontrato con l’ortogonalità che questi hanno con l’approccio comune allo sviluppo di applicazioni, pensiamo sempre in termini di database, query, fetch plan etc. etc… tutti tendiamo a far fatica ad abbandonare la strada nota (con cui abbiamo confidenza) in favore di una completamente diversa (rischiare e buttarsi è difficile) e cerchiamo piuttosto di far andare d’accordo a martellate le due; ammetto che anche io ci ho provato per un po’ ma non funziona c’è poco da fare, è solo controproducente, tutte le volte che con convinzione ci provavo storcevo il naso e avevo quella netta sensazione che qualcosa (tutto?) non fosse al posto giusto.
Finalmente ho capito e credo anche di aver capito che certi passaggi e cambi di punti di vista sia fondamentale farli scontrandosi dal vivo con i problemi (sbattendoci il faccione) perché leggerli su un libro non basta.
.m