Oltre la scrittura di codice
Sviluppare software, di per sé, non è complicato. O meglio… dipende! Se con il termine sviluppare ci limitiamo all’azione di scrivere codice, allora l’affermazione è corretta. Per scrivere del buon codice, oltre che imparare un linguaggio e tutte le best practice per applicare le sue regole, mi basta leggere qualche buon libro sul Clean Code e magari, visto che sono volenteroso, anche qualche libro sulle pratiche dell’Extreme Programming… e il gioco è fatto. Difficilmente mi si potrà dire che ho scritto qualcosa di poco chiaro. Lo si potrà migliorare, senza nessun dubbio, ma non mi si potrà dire che è incomprensibile.
Ma siamo sicuri che sviluppare software sia solo scrivere del buon codice? Il mio docente di ingegneria del software era solito ripetere che la scrittura del codice è solo l’ultima fase nella realizzazione di un sistema. Questa sua affermazione non mi ha mai convinto completamente; all’inizio perché, come ogni novizio introdotto all’arte della scrittura del codice, non vedevo l’ora di veder funzionare su un PC quello che scrivevo, e ogni scoperta, ovviamente, era subito da implementare. Più tardi per altre ragioni, che vedremo più avanti.
Quello che però ho capito è che il professore proprio tutti i torti non li aveva, e che entrambi avevamo un po’ ragione e un po’ torto. Sviluppare software è qualcosa che va oltre la scrittura di buon codice, l’adozione dei pattern architetturali, e l’applicazione delle pratiche di Extreme Programming. Sviluppare software è risolvere problemi di business, e per fare questo dobbiamo prima conoscere il problema; potremmo dire che sviluppare software è un continuo apprendimento, portato avanti da un gruppo di persone: cliente, stakholder, utenti e sviluppatori, di cui il codice è solo l’effetto secondario.
Il senso delle architetture
Sviluppare software dunque è difficile, e lo è maggiormente ora che la parola d’ordine è Distributed Systems. Già, perché già era difficile sviluppare un’applicazione da installare su di un server in locale, figuriamoci sviluppare applicazioni distribuite in cui entrano in gioco attori come microservizi, Cloud, consistenza, elasticità, affidabilità, etc. Tutte queste voci ricorrono sotto la voce architettura, e quando sentiamo questa parola generalmente abbiamo due reazioni; la prima che ci porta a pensare alle architetture più adottate, hexagonal architecture, onion architecture e clean architecture, che è la più rassicurante, perché ci proietta nel mondo delle best-practice, quindi qualcosa di replicabile. La seconda, un po’ meno rassicurante, è quella che ci fa realizzare che è difficile trovare conferenze, o libri, che parlano esplicitamente di architetture, di come configurare l’ambiente per ottenere la massima affidabilità, o il massimo delle prestazioni, senza dover costruire qualcosa di troppo ingombrante sin dall’inizio, e che ci insegni a mantenere l’integrità strutturale nella nostra codebase, senza dover partire con i massimi sistemi sin dall’inizio.
Tra best practice e compromessi
Non esistono le best-practice per questi problemi, ma c’è solo una serie infinita di compromessi, di trade-off. Quello che è buono per la soluzione al problema di oggi, non è detto che sia valido per il problema che dovrò affrontare domani; dipenderà molto da altri fattori, che accompagnano le richieste, decidere quali soluzioni adottare; e in questo caso l’esperienza conta tantissimo. Intendiamoci, è molto più esperto uno sviluppatore che in un anno ha affrontato dieci problemi diversi, rispetto a uno sviluppatore che da dieci anni lavora sullo stesso dominio!
Vent’anni fa, Eric Evans, con il suo iconico libro Domain Driven DesignI [1] ci ha presentato una serie di pattern, tutt’ora validissimi, per affrontare problemi complessi, e non solo. Ci ha mostrato un nuovo modo di affrontare lo sviluppo del software, meno data-centrico e più orientato alla risoluzione dei problemi di business. Pur essendo, come detto, un libro iconico, il testo di Evas dimostra ormai anche tutti gli anni che ha.
Introdurre il Bounded Context
Prendiamo ad esempio uno dei pattern più famosi, e forse più abusati, da lui presentato, il Bounded Context, di cui parleremo in maniera approfondita più avanti. Questo pattern è da molti anni utilizzato come riferimento per la realizzazione di microservizi. Se riesco a individuare una porzione del business isolata, che posso portare avanti indipendentemente dalle altre, magari con un team dedicato, allora ci realizzo un microservizio attorno, e il gioco è fatto. In fondo questo è l’approccio suggerito da Evans: suddividere il dominio in tanti sottodomini indipendenti e, soprattutto, ben chiusi all’interno di un confine invalicabile, dove ognuno è padrone a casa sua. Una delle frasi che da sempre mi gira in testa, facendo riferimento proprio a questo libro, e a questo pattern, è che il dominio è ignorante alla persistenza, ed è anche uno dei concetti che mi impegna di più quando spiego a un team che il pattern del Bounded Context è quello da affiancare al concetto di microservizio.
La pallottola d’argento… non esiste
L’idea, ovviamente, è assolutamente valida. Un conto è occuparsi di problemi di business, e cercare la soluzione software a questi problemi, un altro è occuparsi di problemi di architettura, o infrastruttura, quali database, reti, tempi di risposta, elasticità, etc. In un articolo lungimirante [2], Fred Brooks parla di complessità essenziale vs complessità accidentale, per differenziare appunto le complessità proprie legate al problema di business che intendiamo risolvere, in confronto alle complessità legate all’ambiente in cui distribuiremo l’applicativo, ma anche legate al linguaggio e ai framework che utilizzeremo per svilupparlo.
In questo articolo, Fred Brooks sottolinea che la complessità accidentale è destinata a diminuire, e che gli sviluppatori si possono dedicare maggiormente alla complessità essenziale. Sebbene, insiste Brooks, non esista una Silver Bullet, comunque una serie di innovazioni che affrontano la complessità essenziale può portare miglioramenti significativi nella produzione di software di valore. Anche questo concetto contribuisce a rendere difficoltosa la comprensione moderna di realizzare software di valore; ciò che un tempo era considerato come complessità accidentale, oggi è diventato essenziale, ed altre complessità accidentali si sono aggiunte al nostro lavoro.
Qualche esempio con le architetture
Uno dei compiti di un software architect è quello di mantenere la codebase scalabile, al pari dell’architettura, e per assolvere a questo compito un architect non deve necessariamente conoscere i dettagli del dominio. Ma allora perché, negli ultimi anni, è così difficile realizzare software di valore? Perché fatichiamo a costruire sistemi distribuiti? Perché la complessità è aumentata così tanto con i microservizi? In fondo, nel mondo del software, si fanno sistemi distribuiti dal secolo scorso, non sono una novità portata dal Cloud. Proviamo ad analizzare il problema più da vicino, osservando figura 1.
Mi immagino già l’espressione di molti “Si tratta di un monolite distribuito, un’implementazione sbagliata dei microservizi”; e in effetti, per certi versi, lo è. Ma sono altrettanto certo che in molti siamo passati da questa implementazione, da questa architettura Service-Based. Sono molteplici i benefici che si possono ottenere quando si deve affrontare il refactor di un’applicazione monolite se si adotta questa architettura. Permette a un architect di determinare quali domini necessitano di ulteriore livello di granularità per essere eventualmente trasformati in microservizi e quali sono pronti così come sono.
Non è richiesta la frammentazione del database, almeno non fin da subito, perché non è ancora chiaro quali, e quanti, sottodomini avremo, e come dialogheranno fra loro, e per il momento è molto comodo lasciare l’onere della gestione delle transazioni al nostro database relazionale. Ultimo, ma non meno importante, questo approccio è un approccio di tipo tecnico, che generalmente non richiede il coinvolgimento dei business expert, o degli stakeholder, ma ci consente di intervenire velocemente sulla codebase per ringiovanirla e predisporla a ulteriori miglioramenti.
Il passo successivo verso la frammentazione del nostro monolite potrebbe essere l’individuazione di operazioni cross-domain, ciò che normalmente rappresenta l’accoppiamento fra i servizi, che noi dobbiamo rendere dinamico se vogliamo poi suddividerli, e per questo dovremo aggiungere un orchestratore; magari, in un primo momento, sarà ancora un servizio condiviso fra tutti, e poi potrebbe essere sostituito da un broker di messaggi che gestisce sia le comunicazioni con il Client, sia quelle fra i servizi stessi, garantendo un buon livello di disaccoppiamento, spostandoci verso un’architettura a eventi piuttosto diffusa, EDA Architecture, che rispecchia il pattern del bounded context proposto da Evans, che volutamente ignorava la persistenza dei dati.
Nel 2003 tutto questo era sicuramente rivoluzionario, e per certi versi lo è tutt’oggi; ma ora, nell’asserzione moderna di microservizio, c’è un elemento che cambia completamente le regole del gioco, e che rende essenziale ciò che prima era accidentale, ossia il database. Nei moderni microservizi il database è una parte integrante, è una dipendenza interna al bounded context, perché sappiamo benissimo che per considerare completamente indipendente un microservizio questi deve avere la sovranità assoluta anche dei dati, in modo da poter liberamente modificare la logica di persistenza senza compromettere il funzionamento del sistema nel suo insieme.
Questo è il vero game changer: spostare il database all’interno del perimetro del servizio comporta spostare i problemi relativi ai dati in quelli relativi all’architettura, e l’affermazione “ignorante alla persistenza” cambia di significato. Tutto questo porta altra complessità, perché per garantire una totale autonomia fra i vari microservizi la comunicazione fra di loro dovrà essere asincrona, e questo comporta la gestione di un service bus come dipendenza interna al servizio stesso.
Ma se la comunicazione è asincrona, allora anche la gestione del frontend ne risentirà e si dovrà adeguare a questo nuovo pattern; ecco allora che forse, parlare solo di Bounded Context è diventato quantomeno riduttivo; probabilmente ha più senso parlare di Quantum Architecture, intendendo con questo termine un pezzo di architettura che sia veramente deployabile autonomamente, che sia più simile alla rappresentazione di figura 3.
A questo punto l’analisi si fa più ampia. Ci troviamo di fronte a due problemi strettamente accoppiati, ma che richiedono approcci diversi e, come detto, all’inizio, un continuo scambio di compromessi per adeguare la nostra soluzione, in modo che possa veramente risolvere i problemi di business per cui ci è stata commissionata.
Da una parte dovremo affrontare i problemi legati proprio al dominio, quelli che Fred Brooks, nel suo famoso articolo definisce appunto problemi essenziali, e quindi ci dovremo avvalere dei pattern forniti proprio dal Domain-Driven Design; dall’altra, come architetti, dovremo mantenere l’integrità strutturale nella codebase, e affrontare i problemi accidentali, e magari verificare se nel frattempo sono diventati essenziali.
Da qui
Questo è solo l’inizio del discorso. Dal punto di vista del dominio partiremo facendo chiarezza su problem space vs solution space, e vedremo di capirne le differenze e le modalità di approccio alla questione. Dal punto di vista architetturale ci scontreremo con i problemi legati alle dipendenze che esistono all’interno del perimetro del servizio, ossia come comunicano fra di loro i componenti della nostra Quantum Architecture, quelle che ricadono nell’ambito delle Static Coupling, e delle dipendenze esterne, ossia come comunicano fra loro i vari quanta del nostro sistema, ossia le Dynamic Coupling.
Avremo come fedele alleato il Domain–Driven Design, perché tratta sia i pattern strategici che i pattern tattici, e a noi servono entrambi, ma dovremo andare a bussare anche a qualche altra porta per essere sicuri di realizzare un buon lavoro, per fornire un’applicazione software che risolva veramente il problema del business.
Riferimenti
[1] Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley Professional, 2003
[2] Frederick Brooks, No Silver Bullet. Essence and Accident in Software Engineering. Proceedings of the IFIP Tenth World Computing Conference, pp. 1069–1076.
http://worrydream.com/refs/Brooks-NoSilverBullet.pdf