Abbiamo visto come i microservizi rappresentino spesso un costo eccessivo per un nuovo progetto, o anche per il refactor di un vecchio progetto, e come il monolite può rappresentare un limite alla scalabilità, qualora il nostro sistema ne avesse bisogno.
Una soluzione che sembra contemplare i vantaggi dell’una e dell’altra architettura è la modular architecture. Prima di capire come impostare un progetto seguendo questo tipo di architettura vediamo di capire cosa si intende per modulo e come questo sia conforme ai pattern espressi dal Domain-Driven Design.
Module
Un modulo rappresenta un insieme di oggetti che esprimo e risolvono un concetto di business. Questi oggetti sono altamente coesi fra loro, il che significa che esiste un alto grado di accoppiamento, ma in questo caso non rappresenta un problema, perché non andremo mai a separare questi oggetti gli uni dagli altri. L’importante però è che ogni modulo sia indipendente dagli altri che risolvono altri problemi di business, legati sì allo stesso nostro sistema, ma in un altro contesto. Quindi è fondamentale che sia un basso livello di accoppiamento fra i vari moduli del nostro monolite, in modo da permetterci in futuro, se necessario, di suddividerli in altrettanti microservizi, senza dover riprogettare l’intero sistema.
Modular Architecture e Domain-Driven Design
A questo punto è lecito chiedersi come questa architettura e il DDD si intrecciano, ed allora dobbiamo fermarci un attimo e vedere quali pattern offre DDD e quali stiamo applicando in questo specifico contesto.
Eric Evans, nel suo iconico libro, presenta diversi pattern per lo sviluppo del software orientato al business, più che al dato, e li suddivide in due grandi categorie: Pattern Strategici e Pattern Tattici. I primi ci servono per disegnare quella che ormai tutti conosciamo come la Big Picture del nostro sistema, e ci aiutano a capire come suddividere l’intero Dominio in tanti sottodomini.
Business Domain & Ubiquitous Language
Che cos’è un Dominio? Intanto togliamoci il dubbio principale, non è necessariamente qualcosa legato al mondo del software, anzi, nella maggior parte dei casi non lo è affatto! Il dominio definisce l’area di attività principale di un’azienda, è il servizio che l’azienda fornisce ai suoi clienti, quella che la rende unica di fronte ai suoi concorrenti. Prendiamo ad esempio Uber, il suo dominio è fornire un servizio di trasporto privato facile da accedere e da consumare, sia per chi lo fornisce, sia per chi ne usufruisce. Non è un problema prettamente legato al software, ma è grazie ad un’app ben progettata che noi ne possiamo cogliere il suo valore … ovviamente se siamo all’estero, ma questa è un’altra storia. Volendo ricorrere alla definizione che spesso si trova di dominio legata al Domain-Driven Design possiamo dire che “il dominio è la sfera di conoscenza e attività attorno alla quale ruota la logica applicativa”.
Com’è possibile far combaciare un problema di business, con un buon sviluppo software, sapendo che appartengono a due mondi completamente diversi fra loro? Sapendo che hanno due modelli rappresentativi totalmente diversi? L’idea di Evans è stata proprio quella di creare un modello comune in cui entrambe le parti potessero trovarsi per trovare possibili soluzioni al problema, un modello descritto con termini comprensibili sia dai tecnici che dovranno sviluppare il software, sia dagli stakeholder, dal Cliente e dai suoi utilizzatori, in modo da non creare fraintendimenti nella comunicazione che possono dare origine a soluzioni divergenti. Un modello non è la copia del mondo reale, come spesso noi sviluppatori tendiamo a sotto intendere, ma è un costrutto che ci aiuta a dare un senso ad un dominio complesso; tanto più questa copia è vicina alla realtà, tanto più le decisioni che prenderemo basandoci su di esso saranno corrette e contribuiranno a risolvere il problema. Per poter arrivare ad avere un modello comune fra il mondo del software e quello del business è necessario che entrambi si intendano sui termini utilizzati, senza che ci sia ambiguità sul significato e sul comportamento di ciò che esprimono; questo linguaggio è il primo pattern strategico proposto nel Blue Book, ed è L’Ubiquitous Language.
Ubiquitous Language
Durante il tradizionale processo di sviluppo di un’applicazione software il linguaggio utilizzato dagli esperti del dominio, che rappresenta la conoscenza del dominio stesso, viene “traslato” in un linguaggio tecnico più comprensibile agli sviluppatori, in quella che viene definita come la descrizione dei requisiti del sistema, che essendo appunto il frutto di una traslazione, non rappresenta la comprensione del business e dei suoi problemi. Ogni volta che trasliamo il significato di un termine da un contesto ad un altro perdiamo parte del suo significato, non solo semantico, ma anche comportamentale. L’intenzione, intendiamoci, è buona, ma non il risultato, e ciò che otteniamo è un modello impoverito della realtà, che non potrà che portare a soluzione software incomplete, nelle migliori delle ipotesi, sbagliate nelle peggiori. Trovare un linguaggio comune, che esprima senza ambiguità cosa intendiamo per caffè, ad esempio, è la chiave per un software di successo. Perché ho utilizzato il termine caffè? Perché quando, da buoni italiani, andiamo all’estero ed ordiniamo un caffè, noi già sappiamo che il barista ci ha capiti sul termine, ma non certo sul suo più profondo significato. Per noi il caffè dev’essere ristretto, forte al punto giusto, servito in una tazzina, e per i più accaniti amanti, amaro. Invece il barista ci prepara una tazza alta cinque centimetri, piena di una bevanda dal colore del nostro amato caffè, ma di tutt’altra sostanza. Non è solo il nome che contribuisce a creare l’ubiquitous language, non si tratta di un dizionario, ma di un linguaggio vero proprio, rigoroso quanto un linguaggio tecnico, ma comprensibile a tutti.
Quando diciamo che bisogna restare il più a lungo possibile nel problem space per cercare di comprenderlo sino in fondo, intendiamo proprio questo; riuscire a descriverlo con parole che tutti noi, che saremo chiamati a risolverlo, lo sapremo descrivere in un linguaggio chiaro a tutti, senza ambiguità! Ma allora è sufficiente, per noi sviluppatori, imparare il linguaggio del Dominio ed il gioco è fatto. No!
Domain Language vs Ubiquitous Language
In molte interpretazioni del Domain-Driven Design spesso il linguaggio del dominio, il Domain Language è confuso con l’Ubiquitous Language.
Il domain language è un linguaggio naturale, utilizzato dalle persone che quotidianamente lavorano nell’ambito del dominio stesso. Come recita un brano di Giorgio Gaber
“Le parole definiscono il mondo, se non ci fossero le parole, non avremmo la possibilità di parlare di niente. Ma il mondo gira, e le parole stanno ferme, si logorano, invecchiano, perdono di significato, perdono di senso, e tutti noi continuiamo ad usarle, senza accorgerci di parlare di niente”.
Sicuramente non ricorderemo Gaber per il suo contributo allo sviluppo del software, ma il concetto espresso è molto chiaro a noi che ogni giorno ci dobbiamo confrontare con chi il software ce lo ha commissionato. Un linguaggio di questo tipo, per noi che siamo abituati ad essere rigorosi quando utilizziamo i nostri linguaggi di sviluppo preferiti, è un grosso problema. A noi serve un linguaggio che esprima chiaramente i concetti, che renda esplicito tutto ciò che può essere ritenuto implicito, e questo linguaggio è proprio l’Ubiquitous Language. Un linguaggio costruito e formalizzato da stakeholders e designers per rispondere alle esigenze della progettazione del nostro sistema. Il livello di precisione e di formalità adottati in questo linguaggio dipendono, ovviamente, dal contesto in cui ci stiamo muovendo, progettare un’applicazione per l’acquisto e la vendita di titoli finanziari è diverso dallo sviluppare un’applicazione a supporto di decisioni riguardanti la salute di un paziente.
L’ubiquitous language deve essere preciso e coerente. Deve eliminare la necessità di fare ipotesi e rendere esplicita la logica del dominio aziendale. Non si tratta solo di chiarirci sulle proprietà di un oggetto, ma anche sui suoi comportamenti. L’ambiguità ostacola la comunicazione, per questo motivo ogni termine dell’ubiquitous language deve avere un significato unico.
Ora che abbiamo chiarito cos’è l’ubiquitous language proviamo a metterlo in pratica. Rebecca Wirfs-Brock afferma “A model is a simplified representation of a thing or phenomenon that intenionally emphasizes certain aspects while ignoring others. Abstractions with a specific use in mind”. Se volessimo enfatizzare maggiormente il concetto potremmo vederla come George Box, il quale affermava che “All models are wrong, but some are useful”. Quello che voglio dire è che non esiste un modello universale e nemmeno un linguaggio universale per l’intero dominio, ma solo linguaggi specifici per quel determinato problema di business. L’esempio classico è la mappa; ne esistono di diversi tipi, ognuno per soddisfare uno specifico bisogno
(Learning Domain-Driven Design – Vlad Khononov)
È chiaro che nessuna di queste mappe è in grado di rappresentare il pianeta Terra, viceversa, ognuna di esse contiene solo ed esclusivamente i dettagli necessari al suo scopo, al problema che deve risolvere, ed ovviamente, per ognuna di esse, adotteremo un linguaggio specifico, adatto al caso.
Abbiamo bisogno di creare modelli astratti della realtà perché solo tramite l’astrazione riusciamo a gestire la complessità, omettendo appunto i dettagli non necessari.
Cosa ha a che fare questo con l’ubiquitous language? Quando costruiamo il nostro linguaggio specifico per il problema che dobbiamo risolvere, stiamo effettivamente costruendo un modello del dominio aziendale, precisamente di un sotto dominio aziendale. L’intento, ripeto, è proprio quello di carpire i modelli mentali degli esperti di dominio, i loro processi per implementarli nella nostra soluzione software. Il linguaggio, e di conseguenza il modello, devono riflettere le entità aziendali coinvolte ed i loro comportamenti, le relazioni che intercorrono fra di esse, le cause e gli effetti e gli invarianti. Ma di questo ne parleremo in maniera più approfondita nel seguito.
Un altro aspetto fondamentale, proprio come ricordava Gaber all’inizio del paragrafo, anche l’ubiquitous language, essendo un linguaggio, non è statico, ma evolve. Le ragioni sono infinite, la prima il mercato stesso, che si adegua alle nuove esigenze, plasma il significato dei termini per restare nel presente; pertanto, è fondamentale mantenere un’interazione continua con gli esperti di dominio, ed evitare sempre le supposizioni. Rafforzare il linguaggio, e la sua comprensione, significa abbattere il Debito Tecnico che abbiamo nei confronti del dominio e realizzare soluzioni sempre più appropriate. Una volta stilato un linguaggio comenu, questo deve essere utilizzato in tutti gli artefatti del progetto, dalla documentazione, al codice, proprio per evitare la nascita di ambiguità o fraintendimenti.
A questo punto dovrebbe essere chiaro che una volta trovato l’ubiquitous language del nostro dominio, non ci resterà che suddividerlo in tanti piccoli linguaggi, ognuno specifico per un determinato contesto del business, ed avremo trovato il modo per suddividere il dominio in tanti sottodomini, in tanti Bounded Context, o se preferite, visto da dove siamo partiti, in tanti Moduli. Come individuare questi confini? Trovando i punti in cui uno stesso termine rappresenta lo stesso oggetto, ma dal punto di vista di due contesti differenti. Ad esempio, posso parlare di un articolo dal punto di vista della logistica, quindi mi interessano dove verrà ubicato, i limiti di scorta e riordino, e cose simili, mentre, sempre lo stesso articolo, per un commerciale, avrà bisogno di altre proprietà come il prezzo di vendita, il margine di guadagno, i tempi di consegna e cose simili. Stiamo parlando dello stesso oggetto, ma da due Bounded Context diversi.
Bounded Context
Lo abbiamo già detto in precedenza, il modello che utilizziamo per sviluppare il nostro sistema non è una copia della realtà, ma un costrutto che ci supporta a dare un senso ad un sistema complesso. Questo modello, è giusto chiarirlo sin da subito, non potrà andare bene per sempre, per tutta la durata del nostro sistema. Le esigenze cambieranno, nuove richieste arriveranno dal mercato, e nessuno, all’inizio del progetto, le può prevedere. Non le possiamo prevedere noi, come sviluppatori, basandoci “sulla nostra esperienza” perché ogni progetto è una storia a sé stante, e no le può prevedere il Cliente che ci chiede di realizzare il software. Inutile aggiungere proprietà ad un modello solo perché pensiamo che prima o poi serviranno. Questa è solo una brutta abitudine che abbiamo noi che scriviamo software, e tanto più siamo senior tanto più ci sentiamo autorizzati a farlo. No! Tutte le buone pratiche di sviluppo ci insegnano che dobbiamo fare il minimo indispensabile per risolvere il problema attuale, tutto il resto, tutto ciò che va oltre, è solo un accoppiamento verso un modello, sbagliato, che prima o poi pagheremo caro! Quindi? Quindi rassegniamoci, il modello universale, la Silver Bullet, non esiste. Un modello non può esistere senza dei confini ben precisi, oltre i quali la rappresentazione semplificata della realtà, per cui era stato progettato, non è più valida. Per gli appassionati di filosofia, è quello che i filosofi definiscono come il Platonic fold
“La piega platonica è il confine esplosivo dove il modello platonico entra in contatto con la realtà disordinata, dove il divario tra ciò che si sa e ciò che si pensa di sapere diventa pericolosamente ampio”.
I Bounded Context definiscono l’applicabilità del sottoinsieme del nostro ubiquitous language e del modello che rappresenta. Ci permettono di definire modelli distinti in base a diversi domini di problemi. La terminologia, i principi e le regole di business espresse da un linguaggio, sono coerenti ed hanno valore, solo ed esclusivamente all’interno di questo contesto delimitato. Proprio come nell’esempio dell’articolo citato precedentemente.
Bounded Context vs Sottodomini
Merita una precisione, a mio avviso, la distinzione fra Bounded Context e sottodominio. Apparentemente possono sembrare la stessa cosa, ma nuovamente, la distinzione è fra il concetto di business e la progettazione del sistema. Per comprendere la strategia di business di un’azienda, abbiamo detto più volte, dobbiamo analizzare il suo dominio, e secondo i pattern espressi dal Domain-Driven Design, questa analisi prevede l’identificazione di diversi sottodomini. Così facendo scopriamo come l’azienda lavora e pianifica la propria strategia di mercato.
I Bounded Context sono la rappresentazione, nel nostro sistema, di questo modello, vengono progettati da noi sviluppatori e rappresentano una decisione strategica, ed infatti appartengono ai pattern strategici del DDD. Decidiamo come dividere il modello di dominio aziendale in tanti piccoli modelli meno problematici da gestire. Decidere se suddividere ogni sottodominio in uno, o più, Bounded Context, dipende dalla complessità del modello. Avere una relazione uno-a-uno tra bounded context e sottodomini può essere perfettamente ragionevole in alcuni scenari, mentre in altri possono essere adatte diverse strategie di decomposizione. Nell’architettura del software, purtroppo o per fortuna, non esiste una regola, ma tutto è un compromesso, tutte le decisioni che prendiamo hanno vantaggi e svantaggi, sta a noi decidere quale strategia scegliere in base alla situazione. Non è importante come progettiamo una soluzione, ma perché scegliamo una strategia piuttosto di un’altra.
Laws of Software Architecture
Tipi di Sottodomini
Progettare un sistema orientandoci sul business aziendale comporta, così come nella realtà, la distinzione dei tipi di sottodominio identificati in fase di esplorazione. In un’azienda non tutti reparti sono ugualmente strategici, dipende dal tipo di business dell’azienda stessa, e così dovrà essere anche nel nostro sistema software. In Domain-Driven Design sono classificati tre tipi di sottodominio
Core subdomains
Questo è il tipo di sottodominio che differenza l’azienda dalle altre, dove, strategicamente, si cerca di fare la differenza sul mercato; conseguentemente è dove, a livello di sviluppo, si concentreranno gli sforzi maggiori, si applicheranno tutti i pattern della buona programmazione e, molto probabilmente, il team sarà formato dai migliori elementi. La spiegazione è facile da fornire:
Un core subdomain facile da implementare può fornire un vantaggio competitivo di breve durata, motivo per cui, questo tipo di sottodomini, sono naturalmente complessi.
Generic subdomains
In questa categoria ricadono tutti i sottodomini che tutte le aziende sviluppano allo stesso modo e che non portano nessun vantaggio competitivo al business dell’azienda. Esempi tipici sono i meccanismi di autenticazione e autorizzazione, per cui vale spesso la pena affidarsi a provider esterni, ampiamente testati.
Supporting subdomains
Questa tipologia di sottodomini, così come suggerito dal nome stesso, servono a supporto del business. Non comportano alcun vantaggio competitivo, ma difficilmente si possono acquistare da uno scaffale; perciò, sono spesso implementati internamente.
Integration Patterns
Abbiamo visto come la suddivisione in sottodomini di un modello di business ci consenta di ottenere diversi modelli che possono evolvere indipendentemente gli uni dagli altri, al di là del fatto che ci lavori un solo team o diversi team. Questa proprietà dei Bounded Context è ciò che li ha eletti a pattern per la definizione dello scopo di un microservizio. Detto questo i bounded context non sono di per sé indipendenti. Esattamente allo stesso modo per cui un sistema non è l’insieme dei singoli componenti, ma il modo in cui questi sono assemblati e interagiscono fra loro, allo stesso modo il nostro software non è semplicemente l’insieme dei bounded context che abbiamo identificato, ma il modo in cui questi comunicano, e si scambiano informazioni fra loro. Di conseguenza ci saranno sempre dei punti di contatto tra un bounded context ed un altro, e questi punti di contatto, così come nella vita reale, saranno governati da contratti. La ragione per cui abbiamo bisogno di definire dei contratti per far dialogare fra loro diversi bounded context è semplice, in ogni bounded context è valido uno specifico ubiquitous language, che non è valido per gli altri, altrimenti non li avremmo divisi. Nel momento in cui sorge la necessità di integrare delle informazioni è fondamentale decidere quale linguaggio adottare per l’integrazione stessa. Nel Domain-Driven Design questa problematica è affrontata dal pattern strategico Context Mapping, che suddivide le tipologie di comunicazione in tre gruppi, che rappresentano il tipo di collaborazione in essere: cooperation, customer-supplier, separate ways.
Cooperation
Questo tipo di collaborazione si riferisce a bounded context che hanno una comunicazione ben consolidata, dove il successo dell’uno dipende dal successo dell’altro e viceversa. In questo gruppo troviamo i seguenti pattern
Partnership
In questo modello l’integrazione è coordinata in modo ad hoc. Un team notifica ad un altro le modifiche apportate al proprio modello ed il secondo si adatterà, senza drammi o conflitti, alle nuove modifiche. L’integrazione, in questo caso, è a doppio senso, non esiste un team, o in generale, un bounded context, che detta le regole. Ognuno lavora in maniera indipendente dall’altro, ma in perfetta armonia per quanto riguarda lo scambio di informazioni. Questo tipo di collaborazione richiede una costante sincronizzazione ed un alto livello di comunicazione, motivo per cui è valido se i team sono vicini, geograficamente parlando, fra loro.
Shared Kernel
Quando una parte di un sottodominio è comune a più sottodomini allora è conveniente creare un modello condiviso fra più bounded context. È fondamentale accettare il fatto che il modello condiviso deve essere consistente attraverso tutti i bounded context interessati; questo rappresenta sicuramente un accoppiamento, e come sempre, bisogna valutarne pro e contro prima di implementarlo. Esistono delle linee guida per aiutarci a prendere questa decisione che possiamo riassumere in questo breve elenco
I consumatori del modello condiviso
- Perderanno delle capacità?
- Acquisteranno delle capacità?
- Potranno concentrarsi maggiormente sulle loro scelte strategiche?
- Saranno rallentati da questa dipendenza?
- Potranno rifiutare la migrazione al modello condiviso?
- Il team di sviluppo del modello condiviso sarà reattivo?
- Il numero di modelli dipendenti sarà problematico?
- Il costo di migrazione sarà eccessivo?
Come al solito, non esite una legge, ma dipende dal contesto.
Customer-Supplier
Il secondo gruppo di pattern di collaborazione prende il nome di customer-supplier, e come evidenziato nell’immagine, uno dei bounded context, il supplier, fornisce un servizio per il suo customer. In questo gruppo, a differenza del precedente che prevedeva una collaborazione, entrambi i team (upstream e downstream) possono avere successo indipendentemente l’uno dall’altro. Per questo motivo spesso si verifica uno squilibrio di potere ed uno dei due team può dettare le regole per il contratto di integrazione. I pattern che appartengono a questo gruppo sono
Conformist
In alcuni casi è il team upstream a non avere alcuna motivazione per supportare i team downstream, e si limita a fornire un contratto di integrazione che agli altri non resta che accettare (prendere o lasciare). È il caso in cui ci dobbiamo integrare con qualche servizio esterno, su cui non abbiamo nessun potere di contrattazione. Se il team downstream può accettare il contratto di integrazione così come fornito, allora il rapporto è definito conformist.
Anticorruption Layer
Anche in questo è il team upstream a forzare le regole del contratto di integrazione, ma a differenza del pattern conformist in questo caso chi si trova a valle, in posizione downstream, non è in grado di accettare il contratto per svariate ragioni: cambiamenti frequenti, inefficienza nella gestione del contratto, protezione del bounded context che si trova in downstream. L’Anticorruption Layer funge da interprete fra il contratto esterno ed il nostro bounded context.
Open-Host Service
Anche in questo caso il potere decisionale è sbilanciato verso chi sta a monte, ma a differenza del pattern precedente, è proprio chi sta a monte che protegge i propri consumatori separando l’implementazione interna del contratto da quella esterna. Questo disaccoppiamento consente al bounded context a monte di evolvere il proprio modello di implementazione e quello esterno di integrazione indipendentemente.
Può succedere di dover mantenere più di una versione del contratto esterno, per dare il tempo ai vari consumatori di adeguarsi alle nuove specifiche. In questo caso il bounded context a monte può esporre più versioni del contratto di integrazione. Un esempio classico è rappresentato dall’esposizione di API REST da parte di un Bounded Context, delle quali possono esistere più versioni.
Separate Ways
Esiste un terzo gruppo di collaborazione, ed è quello che non prevede collaborazione affatto. Vediamo quali sono i casi che rientrano in questo gruppo
Communication Issues
Quando i team hanno difficoltà a comunicare oppure a collaborare, anche per ragioni politiche interne all’organizzazione, può essere conveniente che ognuno vada per la propria strada, e quindi si preferisca duplicare alcune funzionalità in più bounded context.
Generic Subdomains
Esistono funzionalità che sono più facili da duplicare che non da integrare, soprattutto nei sottodomini generici, che normalmente sono molto facili da sviluppare, oppure da acquistare ed integrare. Il logging è una di queste funzionalità.
Model Differences
Anche in questo caso, in cui le differenze fra i modelli di diversi bounded context sono talmente evidenti, conviene duplicare le funzionalità piuttosto che investire tempo nell’integrazione. Non è consigliabile prendere questa strada nel caso i sottodomini siano dei core subdomains e la ragione dovrebbe essere abbastanza chiara, duplicando funzionalità a questo livello si rischierebbe di andare in contrasto con la strategia aziendale che tende ad ottimizzare il processo concentrando gli sforzi maggiori proprio a questo livello.
Context Map
Il context mapping è una rappresentazione visiva dei bounded context a delle relazioni che intercorrono fra di loro, e ci fornisce preziose indicazioni strategiche. Ad esempio, ci fornisce una panoramica dei componenti e dei modelli che implementano (High-level design), ma anche il modello di collaborazione in essere fra i vari team, evidenziando quelli che collaborano e quelli che preferiscono prendere strade autonome (Communication patterns), infine, ma non certo meno importante, ci fornisce uno specchio dei problemi organizzativi che esistono all’interno dell’organizzazione (Organizational issues), quando, ad esempio, ci accorgiamo che tutti i rapporti di collaborazione sono di tipo Separate Ways o Anticorruption Layer.
Conclusioni
Partendo dal pretesto della spiegazione dell’architettura modulare abbiamo visto come Domain-Driven Design ci supporta nell’aspetto strategico dello sviluppo del software. Spesso l’applicazione di questi pattern è vista come la strada per la realizzazione di sistemi a microservizi, ma come abbiamo visto non è scritto da nessuna parte che questo debba essere necessario. Come spesso ripeto, nel 2003, anno di pubblicazione del libro di Eric Evans, non solo non si parlava di microservizi, ma l’unica architettura conosciuta era la Layer Architecture, eppure tutti i pattern visti qui erano applicabili.
Un esempio pratico di come applicare questi concetti ad un software reale lo potete trovare qui.