Un po’ di storia
Abbiamo già enunciato le leggi che regolano le architetture software, e la seconda di esse recita:
Why is more important than how
Perciò vediamo di capire perché dovremmo sempre aspirare a implementare soluzioni event-driven.
Correvano gli anni Settanta, proprio all’inizio di quel decennio, quando Alan Kay, uno dei padri della programmazione a oggetti, insieme ad altri illustri colleghi dello Xerox PARC, creò SmallTalk, un linguaggio orientato agli oggetti. Quindi? In molti, se non tutti, già sapevano di questa storia, ma quello che spesso tutti ci dimentichiamo, sono i principi che lo stesso Alan Kay enunciò a proposito di OOP:
- ogni cosa è un oggetto;
- gli oggetti comunicano fra di loro inviando e ricevendo messaggi;
- più gli gli oggetti contengono dati e comportamenti;
più gli altri punti che potete trovare comodamente in rete.
È curioso scoprire che anche Carl Herwitt, autore del modello Actor Model [2], arrivò alle stesse conclusioni, sostituendo l’oggetto con il concetto di “attore”, pur senza conoscere né la persona, né le ricerche di Alan Kay.
Se poi volessimo andare ancora un po’ più a ritroso nella storia, scopriremmo di questo trattato, di cui riporto solo alcuni punti, scritto nel 1922:
- il mondo è tutto ciò che si verifica;
- il mondo è la totalità dei fatti, non delle cose;
- il mondo è determinato dai fatti e dal loro essere tutti i fatti;
- il mondo si divide in fatti;
- qualcosa può verificarsi o non verificarsi e tutto il resto rimane uguale.
Se vi state chiedendo quale genio dell’informatica fosse così avanti nei tempi tanto da avere anticipato persino il concetto di Event Sourcing, fermatevi… si tratta di un trattato filosofico [3].
Già queste informazioni cominciano a rispondere alla domanda del “perché dovremmo creare sistemi event-driven?”.
Lo scenario attuale
Tornando a tempi più recenti, provate a pensare come i sistemi distribuiti stanno plasmando il nostro modo di creare applicazioni. Il cloud computing, in maniera particolare, ha influenzato profondamente il modo di consumare informazioni, non solo per i nostri sistemi, ma anche per noi stessi.
Non esiste business che non sia a portata di app oggigiorno. Abbiamo bisogno di scaricare e inviare dati non solo all’interno dei nostri applicativi, ma dobbiamo essere aperti anche al resto del mondo se veramente vogliamo che il nostro sistema abbia successo. Dobbiamo fare in modo che i nostri dati siano fruibili, “consumabilii” anche da altri, con le dovute precauzioni, ma dobbiamo essere aperti. Tutto questo ha spostato il modo di progettare il software, sdoganando definitivamente la programmazione asincrona, e mandando in pensione il concetto di programmazione sincrona, se non per piccole applicazioni local, sempre che ancora esistano…
Dobbiamo avere la certezza di poter consumare le informazioni nel momento in cui ci servono, non quando vengono prodotte, e questo comporta che tutti i messaggi che produciamo, o a cui facciamo un subscribe per riceverli, devono avere la possibilità di persistenza, affinché sia possibile consumarli al bisogno. Del resto, è proprio quello che accade nella vita reale fra persone reali, esattamente come descritto da Alan Kay e Carl Herwitt nelle loro ricerche.
Questo cambiamento nel modo di gestire i dati ha provocato radicali cambiamenti a livello di architetture software, ma soprattutto a livello di relazioni e di business nella società. Aspettate… stiamo forse dicendo che Conway [4] aveva ragione?
Che cosa sono i microservizi Event-Driven
I microservizi e le architetture a microservizi esistono da molti anni, intendiamoci, in diverse forme e sotto differenti nomi. I “diversamente giovani” ricorderanno SOA, Service-Oriented Architectures, in cui regnava la comunicazione sincrona fra diversi servizi e i messaggi venivano scambiati in una comunicazione diretta fra due specifici servizi distribuiti.
La comunicazione event-driven non è quindi la novità, ma le esigenze sono cambiate: ora sono intervenuti termini come big data, real-time, scaling e la comunicazione sincrona è diventata insostenibile; non possiamo pensare di avere un insieme di sistemi, tutti contemporaneamente e costantemente, sincronizzati, basta pensare alle Fallacies of distributed computing [5] per averne la prova.
In una moderna architettura a microservizi basata sugli eventi, questi ultimi non vengono distrutti dopo essere stati consumati, ma vengono persistiti per poter essere consumati da altri servizi che, per qualsiasi motivo, potrebbero non essere disponibili al momento in cui gli eventi stessi sono stati emessi. Questa distinzione è fondamentale per capire come si sono evolute le architetture dei sistemi distribuiti, e come sono cambiate le tecnologie a corredo o, facendo riferimento a alle definizioni di Fred Brooks [6], come certe complessità accidentali siano diventati essenziali.
Certamente il primo cambiamento lo notiamo a livello tecnologico, perché è il primo che, da tecnici, siamo chiamati a risolvere; ma è sempre il motivo dietro le quinte, il famoso “perché lo fai?” che ci indica la direzione. Le architetture a microservizi basate su eventi portano con loro un cambiamento di paradigma. Ora scriviamo i microservizi attorno alle necessità del business, e facciamo in modo che ogni microservizio si occupi di quel pezzo di business della nostra applicazione, concetti che in SOA non erano nemmeno considerati, se non da pochi illuminati.
Altra questione importante: ora ci preoccupiamo che la nostra applicazione, nel suo insieme, non si blocchi se, per qualsiasi motivo, una parte di essa non è disponibile e non può ricevere messaggi. Accettiamo il fatto che alcune funzionalità non siano disponibili, ma non che l’intero sistemi sia offline, e questo è possibile solo ed esclusivamente se le comunicazioni fra le varie parti del sistema stesso sono asincrone.
Domain-Driven Design, Bounded Context e microservizi
Sembra quasi il preludio di un film tipo “I Tre Moschettieri”. In effetti Domain–Driven Design ha avuto il suo più grande momento di gloria e di avvio al successo proprio con l’arrivo delle architetture a microservizi. L’ho già detto che non esiste una relazione uno-a-uno fra Bounded Context e microservizi, ma è altrettanto innegabile che questi due pattern hanno parecchio in comune, e che questo pattern del DDD è sicuramente il più utilizzato per l’isolamento del problema di business che poi viene implementato nel rispettivo microservizio.
Proviamo a pensare a un’azienda, di un qualsiasi settore, e sicuramente troveremo un reparto commerciale, un reparto vendite, un reparto supporto clienti,e così via. Questi reparti sono spesso associati, nell’operazione di Context Mapping, ai rispettivi Bounded Context nel sistema che dovrà gestire l’azienda stessa. Questa frammentazione in sottodomini ovviamente può continuare ulteriormente dopo questo primo passaggio, per creare ulteriori sottodomini, più granulari, che ci aiuteranno a isolare i vari problemi di business che dovremo risolvere, e a creare quindi team indipendenti per la loro gestione, che creeranno microservizi indipendenti gli uni dagli altri, che potranno evolvere in maniera autonoma secondo le necessità dei singoli contesti. Ma siamo certi che i nostri microservizi siano realmente indipendenti gli uni dagli altri?
Coupling vs Cohesion
Quando suddividiamo il dominio della nostra applicazione in tanti sottodomini, l’operazione più difficile è sempre quella di individuare prima, e gestire dopo, le dipendenze fra di essi. Ricordiamoci che il fatto di suddividere un dominio non ci autorizza a creare tante applicazioni isolate l’una dalle altre: questo è quello che facciamo noi sviluppatori dietro le quinte, ma l’utente che utilizzerà la nostra applicazione si aspetta di usare appunto una applicazione, non tante applicazioni! In un modo o nell’altro, questi microservizi, come abbiamo visto prima, devono comunicare.
Microservizi indipendenti
Intanto chiariamo che, per essere realmente indipendente, un microservizio deve avere il proprio database, o i propri database, se distinguiamo un database per le normali operazioni di lettura e uno per la persistenza degli eventi, nel caso volessimo implementare il pattern CQRS-ES. Dovrà avere accesso a un sistema di trasporto dei messaggi per comunicare con l’esterno, e dovrà, possibilmente, rispondere a una parte della UI a esso dedicata.
Soltanto se siamo in grado di garantire tutto questo isolamento potremo veramente affermare che la nostra architettura è un’architettura a microservizi, in caso contrario, staremo implementando un “monolite distribuito”, che può anche essere un passaggio intermedio della nostra evoluzione, ma non l’obiettivo finale.
Quindi? Tutti i microservizi sono disaccoppiati fra loro? Non esattamente. All’interno di ogni microservizio ogni oggetto può tranquillamente dipendere da un altro appartenente allo stesso microservizio. Questa situazione è totalmente accettabile, in quanto stiamo parlando di un componente, il microservizio, che quando cambia, lo fa nel suo insieme; e tutti i test implementati al suo interno ci garantiscono che tutte le modifiche che apportiamo per implementare nuove feuture non rompono il suo funzionamento. Il discorso cambia quando parliamo di relazioni con gli altri microservizi.
Comunicazione asincrona
Tornando a quanto detto riguardo a SOA e alla comunicazione sincrona fra i vari servizi, ora ci appare chiaro perché, in una moderna visione di un sistema distribuito, questo tipo di comunicazione non può più essere accettato. Se la comunicazione fra due microservizi fosse sincrona, significherebbe avere una dipendenza fra i due, il che ci porterebbe ad affermare che ogni cambiamento apportato a uno dei due, quasi certamente, porterebbe alla necessità di cambiare anche l’altro, impedendo così la possibilità di rilasci indipendenti.
Ricordate quando abbiamo trattato la necessità di separare gli eventi che vengono consumati all’interno di un Bounded Context (Domain Event) da quelli che vengono condivisi con gli altri Bounded Context (Integration Event)? Bene, ora sostituite Bounded Context con microservizio e tutto sarà ancora più chiaro. Se l’evento che un microservizio emette verso l’esterno è diverso da quello che consuma internamente, allora avrò sempre la possibilità di apportare modifiche, ossia cambiare versione, a quello interno, senza la necessità di avvisare chi consuma quello esterno del cambiamento, perché quest’ultimo potrà restare invariato. In questo modo abbiamo rimosso la dipendenza fra due, o più, microservizi. Ricordate il Principio di Robustezza, o legge di Postel? Dobbiamo essere rigidi nel cambiare i contratti verso l’esterno, perché questi rappresentano la dipendenza verso altri sistemi, e se li cambiamo, dobbiamo necessariamente avvisare chi li consuma, dobbiamo riscrivere i nostri contract test e riallineare l’intero sistema; in pratica dobbiamo dire addio all’indipendenza del rilascio!
E nel caso invece dovessimo apportare modifiche anche all’evento di integrazione? Nessun problema, una delle caratteristiche delle Architetture Evolutive è proprio quella di considerare normale mettere a disposizione versioni diverse dello stesso servizio, quindi potremo tranquillamente emettere due integration event, uno per chi ci ha chiesto la nuova versione, e l’altro per chi non ne ha bisogno, in modo che possa continuare a consumare la versione precedente.
Conway’s Law e Strutture di Comunicazione
La cosiddetta legge di Conway [4] recita:
Organizations which design systems […] are constrained to produce designs which are copies of the communication structures of these organizations.
Questa famosa citazione di Melvin Conway, risalente al 1968, è spesso citata quando si parla di sistemi distribuiti e, pur essendo stata scritta in tempi in cui architetture simili non erano nemmeno lontanamente pensabili, è assolutamente adeguata.
Nell’esempio che ho fatto prima della divisione in sottodomini, ho descritto delle aree di business all’interno di un’azienda generica; quando si organizzano i team attorno a queste aree di business, questi team finiscono col produrre servizi che sono delimitati dai loro confini di business, ossia, tendono a condividere informazioni tipiche del loro contesto, nel bene e nel male. Qui si potrebbero aprire interi capitoli su quali informazioni condividere e quali no, ma non è il nostro scopo.
Informazioni condivise e livelli di indipendenza
Sicuramente le informazioni che ogni Team condivide con gli altri determinano il livello di indipendenza che ogni Team sarà in grado di costruirsi. Proviamo a fare un esempio pratico.
Se il eam che si occupa del sotto dominio del magazzino non condivide le giacenze dei prodotti, sarà impossibile per gli altri team costruirsi una copia delle stesse nei propri database, in modo da non dipendere sempre da questo microservizio ogni qualvolta si avrà la necessità di conoscere la disponibilità, o meno, di un prodotto. Se, ogni volta che il team del reparto vendite deve implementare una funzionalità sugli ordini di vendita, deve chiedere al team — leggi microservizio — del magazzino se il prodotto è disponibile, significa che fra i due microservizi esiste una dipendenza. La potremo risolvere in tanti modi diversi, non necessariamente con una comunicazione sincrona, ma la dovremo risolvere per fornire al nostro cliente una data di consegna prevista.
Inverse Conway Maneuver
Un’interessante esplorazione di questo problema è stata fatta da Martin Fowler, che tratta il problema e propone una soluzione interessante denominata Inverse Conway Maneuver [7].
Esplorare nuove modalità all’interno dell’azienda
Cambiare i pattern di comunicazione all’interno di un’azienda non è affatto banale. Significa cambiare equilibri delicati costruiti negli anni, significa abbracciare un nuovo modo di affrontare i problemi, in cui, quasi sicuramente alcune figure vedono diminuire il loro potere all’interno del palinsesto aziendale.
Se fino a ieri, per poter confermare una data di consegna, dovevi chiedere a me, responsabile del magazzino, la disponibilità del prodotto, e ora invece hai la tua autonomia, e puoi chiudere l’ordine autonomamente, significa che io ho perso valore? No, significa che è cambiato il modo di comunicare, significa che stiamo esplorando nuovi modi, stiamo esplorando, e magari scopriremo che non è la soluzione migliore quella implementata, ma saremo pronti a percorrere nuove strade. Ricordate quando abbiamo parlato di Architetture Evolutive? Una delle sfide è proprio quella di abbracciare una cultura della sperimentazione, in cui fallire viene letto come provare, e non ci fa paura.
In ogni caso non sempre è facile ignorare i confini esistenti all’interno delle organizzazioni, e non è detto che farlo sia un bene. Procedere per piccoli passi durante un refactoring supporta non solo l’apprendimento del dominio su cui stiamo lavorando, ma permette a chi ci chiede supporto, di accettare nuovi modelli di comunicazione e di condivisione.
Conclusioni
La comunicazione è la base delle relazioni, non solo umane, ma anche dei sistemi che sviluppiamo, e senza di essa sia i primi che i secondi, sono destinati a fallire, sia pure in termini diversi.
Le strutture di comunicazione determinano il modo in cui il software viene creato e gestito durante il suo ciclo di vita, e del ciclo di vita di chi lo utilizza. Le informazioni che decidiamo di condividere fra i nostri sistemi, così come nella vita reale, determinano il grado di indipendenza, o di dipendenza, di ogni servizio rispetto agli altri; e questo è un dato di fatto di cui tener conto durante le operazioni di ristrutturazione di un applicativo esistente. Non possiamo pensare di rimodellare tutto in un colpo solo: i piccoli cambiamenti sono più facili da accettare.
Riferimenti
[1] Adam Bellemare, Building Event-Driven Microservices. O’Reilly, 2020
[2] Actor Model
https://en.wikipedia.org/wiki/Actor_model
[3] Ludwig Wittgenstein, Tractatus Logico-Philosophicus. Kegan Paul, Trench, Trubner & Co., 1922
https://t.ly/6XHaZ
[4] Legge di Conway
https://en.wikipedia.org/wiki/Conway%27s_law
[5] Fallacies of distributed compunting
https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing
[6] Frederick P. Brooks Jr., No Silver Bullet. Essence and Accident in Software Engineering. 1986
https://worrydream.com/refs/Brooks_1986_-_No_Silver_Bullet.pdf
[7] Martin Fowler, Conway’s Law
https://martinfowler.com/bliki/ConwaysLaw.html
Sono fondamentalmente un eterno curioso. Mi definisco da sempre uno sviluppatore backend, ma non disdegno curiosare anche dall'altro lato del codice. Mi piace pensare che "scrivere" software sia principalmente risolvere problemi di business e fornire valore al cliente, e in questo trovo che i pattern del DDD siano un grande aiuto. Lavoro come Software Engineer presso intré, un'azienda che sposa questa ideologia; da buon introverso trovo difficoltoso uscire allo scoperto, ma mi piace uscire dalla mia comfort-zone per condividere con gli altri le cose che ho imparato, per poter trovare ogni volta i giusti stimoli a continuare a migliorare.
Mi piace frequentare il mondo delle community, contribuendo, quando posso, con proposte attive. Sono co-founder della community DDD Open e Polenta e Deploy, e membro attivo di altre community come Blazor Developer Italiani.