Progettazione: da un approccio tradizionale ad uno più modulare

Ormai è parecchio che non bloggo più: vuoi per scadenze importanti e molto (troppo) ravvicinate, vuoi per necessità di dedicare più tempo alla famiglia. Resta il fatto: sono "assente" da qualche mese, ma non sono stato certo con le mani in mano ;)...

Da parecchio tempo sto cercando di dare un'impronta differente ai progetti a cui lavoro. Che siano di piccole dimensioni oppure più corposi, la necessità è quella di avere sistemi che siano facilmente manutenibili, efficienti e il più possibile flessibili; senza però impattare sulle prime due caratteristiche desiderate.

Il "mondo a servizi" disaccoppia sempre di più la user interface dagli strati applicativi dedicati alla persistenza e all'elaborazione. La tendenza, anche dei grandi competitors presenti sul mercato, è quella di realizzare piattaforme "internet-based" che siano autoconsistenti ed agnostiche; in pratica capaci di erogare un servizio e una funzione, indipendentemente da quelli che saranno i device e gli ambienti operativi sui quali il servizio stesso sarà fruito. E' così che le varie Google API e Microsoft Azure Services (solo per citarne alcuni) premono sempre più in questa direzione: "Io ti fornisco un servizio che fa questa cosa, ma alla presentazione e all'integrazione ci devi pensare tu, sviluppatore".

Personalmente lo considero un vantaggio, perchè l'interfaccia utente che va bene per tutti i possibili utilizzatori e tutti i possibili terminali, è mera un'utopia. Ci sarà sempre il "rompimaroni" (passatemi il termine tecnico) che vorrà avere qualche cosa di diverso, a cui non interesserà quella funzione, oppure che vorrà dare maggior risalto all'altra solo perchè la utilizza più di frequente. In un mondo dove l'utente è messo al primo posto, il disaccoppiamento e la modularità devono essere un "must", mettendo definitivamente da parte le applicazioni monolitiche a favore di una miriade di micro applicazioni, e micro-servizi, in grado di parlare tra loro per raggiungere l'obiettivo comune.

Dal punto di vista di un software architect - mi piace pensare di esserlo, anche se sono a tutti gli effetti uno sviluppatore, a cui piace mettere le mani in pasta - la modularità è cosa buona e giusta. Che permette in primis non solo un incremento notevole della manutenibilità dell'intero apparato, ma anche la possibilità di "svecchiare" in maniera periodica uno o più componenti che arrancano, o che magari semplicemente necessitano di una serie di miglioramenti. Aggiungere nuove funzionalità è poi infinitamente più semplice, perchè l'architettura è pensata per essere in continua espansione, e i punti di ingresso per il flusso applicativo sono una moltitudine.

Ed è così che mi sono trovato di fronte ad un'applicazione, anche piuttosto semplice, che fino a qualche anno fa avrei organizzato - secondo la tradizione - con un database più relativo OR/M, alcune classi per la gestione del flusso applicativo, e la parte di presentazione con il classico ASP.NET WebForms. Ho tentato di approcciarla in maniera differente, stravolgendo quelli che sono i concetti che mi hanno guidato negli ultimi 10 anni.

Non sono libero di divulgare i contenuti funzionali del sistema - c'è la pena di morte, mi dicono - ma posso inventarmi uno scenario che sia il più analogo possibile dal punto di vista tecnico. Ammettiamo di avere un portale di e-commerce, con una serie di prodotti e loro caratteristiche dettagliate; naturalmente c'è la possibilità di fare acquisti dietro verifica della giacenza in magazzino, e visualizzazione di una eventuale data di nuova disponibilità del prodotto nel caso un cui l'elemento fosse esaurito. Il modulo "magazzino" non è per niente semplice: gestisce ingresso ed uscita merce, ordini dei fornitori e dei clienti...e tutte quelle belle cosine di cui non sto a parlarvi - anche perchè non le conosco, visto che è solo un esempio ;).

A fronte di una semplice ricerca, penso possiare immaginare la schermata dei risultati che dovrebbe apparire all'utente del portale: una banale lista di prodotti - "of course" corrispondenti ai criteri di ricerca - accando a ciascuno dei quali è riportato il pulsantino "Compra", oppure la suddetta data di prossima disponibilità per giacenza a magazzino uguale a zero. Bene, partiamo da qui...

Un approccio tradizionale - la faccio semplice - prevederebbe una query in "join" tra la tabella dei prodotti e quelle coinvolte nel modulo magazzino (ordini, giacenze, ecc). La complessità della query di estrazione chiaramente dipende dalla complessità del relativo modulo "owner" dei dati (il magazzino stesso). Questa dipendenza - a tutti gli effetti - "lega" il portale di e-commerce a quest'ultimo, impendendo quindi il concetto di modulatità e "separation of concerns" che abbiamo auspicato. Inoltre (cosa non di poco conto) lo sforzo elaboravo della base dati per estrarre il semplice dato di "quantità disponibile" è considerevole ed impatta negativamente sul tempo che trascorre tra la "request" dell'utente (submit dei criteri di ricerca prodotti) e la response del server (visualizzazione dei risultati).

Ora mi domando: è veramente necessario che ogni volta che faccio un ricerca di prodotti, le informazioni siano calcolate in tempo reale? Se applico due volte lo stesso criterio di ricerca, ottenendo gli stessi risultati, è giusto che il database sia stressato due volte, andando ad impattare sulla "user experience" che viene percepita dall'utente? Il processo è ottimizzato in tutto e per tutto? E' scalabile? Per tutte queste domande, la risposta che mi do - ed è solo la mia opinione personale - è no.

Passiamo ai dettagli tecnici, dicendo subito che l'OR/M c'è ancora: non sono tanto pazzo nel 2014 da mettere ancora le mani nelle query SQL "raw", se non è strettamente necessario. Ma il disegno della base di dati, a fronte delle precedenti considerazioni non è lo stesso che avrei "buttato giù" seguendo il modello tradizionale. Come molti sviluppatori, sono cresciuto con il concetto di schema di dati "normalizzato", cioè ottimizzato per fare in modo che la stessa informazione non fosse presente in due diverse locazioni del sistema di persistenza. Questo concetto - giustissimo nella maggior parte delle situazioni - talvolta va a cozzare con quella che è la caratteristica principare desiderata dai sistemi moderni: la scalabilità e la reattività.

Tornando alla pagina di risultati citata poco fa, perchè devo attendere - sparo un numero casaccio - 350 ms per calcolare le giacenze di magazzino di una decina di prodotti che corrispondono ai criteri di ricerca, quando posso trovare un modo per erogare lo stesso dato in 35 ms?

Il tutto si ottiene con il concetto di "dato stale" (letteralmente tradotto, sarebbe "dato in stallo"). Si tratta di una pratica molto disprezzata in passato, che si sta rivalutando parecchio negli ultimi tempi nel campo dei "BigData" e, in generale, in tutti i settore in cui la mole di dati è talmente consistente da non permettere di poter essere processata in real-time, soprattutto da un singolo processo applicativo. Si basa su un concetto estremamente semplice, una domanda, fondamentalmente: è necessario che questo particolare dato sia aggiornato esattamente nell'istante in cui si va a consumare?

Nel nostro caso, possiamo girare la domanda: quando il valore di disponibilità/indisponibilità di un prodotto varia? La risposta è ancora una volta banale: varia quando viene venduto un prodotto, oppure quando si registra una nuova entrata merce nel modulo magazzino. Per quel particolare articolo è necessario ricalcolare il numero di elementi disponibili; e ancora, se questa quantità calcolata è zero, deve essere interrogata un'altra area del "magazzino" per conoscere la data di disponibilità sulla base delle previsioni di consegna della nuova merce.

E' semplice capire come questi due dati (la quantità e l'eventuale data di prossima disponibilità), rappresentano una sorta di "ridondanza" di informazione all'interno del sistema; la loro nuova collocazione sarà in due colonne aggiuntive sulla tabella dei prodotti, diventando di fatto parte dell'informazione atomica del record di prodotto stesso. Questo trasforma, di fatto, un database "normalizzato" in uno "non normalizzato", proprio perchè il medesimo dato è ottenibile eseguendo due differenti processi estrattivi.

Con questa modifica della struttura del nostro database, è possibile realizzare una estrazione dei risultati di ricerca con una semplice query sulla tabella dei prodotti, senza interrogare un'altra decina di tabelle applicative per ottenere la stessa informazione. L'accortezza che deve essere intrapresa, naturalmente, è assicurarsi che venga in messo in moto il processo "update" dell'informazione ridondata allo scatenarsi degli eventi descritti in precedenza.

Un esempio molto noto di "dato stale" è presente su Twitter. Quando inviate una richiesta di "follow" di un particolare utente, scatenate un evento asincrono della piattaforma applicativa che innesca la rivalutazione, sia del vostro profilo (al quale dovrà essere applicato un incremento del contatore di persone che seguite), sia del profilo della persona destinataria del "follow" (al quale, per simmetria, dovrà essere incrementato il contatore dei followers). L'aggiornamento dei due contatori non nè immediato, e certamente non è avviato come processo sincrono dal servizio API di Twitter nel momento in cui cliccate sul pulsante presente sull'interfaccia utente della vostra app. E se così fosse, immaginatevi che a quale immenso sforzo computazionale sarebbero sottoposti i web server di Twitter per insistere sui layer applicativi, e ancora sulle basi di dati sottostanti (anche se queste ultime, chiaramente, non hanno un database engine relazionale, ma qualche cosa di più adatto allo scopo).

Ritornando al nostro esempio del sito di e-commerce è necessario prendere in considerazione questa possibile pericolosa situazione. L'utente "A" apre il suo browser, esegue una ricerca dei prodotti, e visualizza a video i risultati (comprensivi delle nuove informazioni di disponibilità lette direttamente dalla tabella di anagrafica). Un secondo utente "B", esegue la medesima ricerca e clicca sul pulsante "Compra" per uno dei prodotti con disponibilità 1 elemento. L'utente "A" (la fortuna è cieca, ma la sfiga ci vede benissimo) mette nel carrello lo stesso prodotto che però non ha disponibilità nonostante il dato di giacenza che ha visualizzato riportasse 1 unità a magazzino. E' chiaro che solo un meccanismo di compensazione è in grado di risolvere la questione, magari notificando al secondo utente che il prodotto non è più disponibile oppure che il suo ordine è stato annullato.

Diciamo subito che il concetto di "stale data", cioè dato non aggiornato, potrebbe portare a situazione applicative non previste; e in generale non è una pratica che possibile mettere in atto in ogni situazione, se non intervenendo in maniera "compensativa", e prendendo in considerazione tutti gli scenari che ne possono derivare. Ma in certe situazioni è meglio avere un dato che non rappresenta la situazione reale nel momento della richiesta, piuttosto che essere necessari ad attendere l'elaborazione del dato stesso per poter fruire l'intero contenuto. In generale, in una architettura a servizi, avere un componente che risponde ad una richiesta esterna in un periodo di tempo non accettabile, rappresenta un enorme collo di bottiglia che impatta sull'intero apparato e su come il sistema viene percepito dall'utente finale.

Finisco quindi ricollegandomi alla premessa di questo post. Un mondo a servizi, dove le architetture sono modulari e ciascuna entità vive di vita propria collaborando con le altre, porta a pensare in maniera molto diversa le soluzioni che studiamo; libere da schemi, certezze che ci hanno guidato per anni, ma dando più spazio all'immaginazione e agli obiettivi: "l'utente al primo posto", se vogliamo ridurre il tutto ad una sola frase.

Insomma...pensate in maniera differente (cit.) ;)
M.

Commenti

Post popolari in questo blog

Cancellazione fisica vs cancellazione logica dei dati

RESTful Stress: misurare le performance di un servizio REST

Load tests, Stress tests e performance di un servizio REST