CAPSULE
Introduzione al Domain-Driven Design
Progettare il software partendo dal dominio
Il Domain-Driven Design non parte da framework, database o microservizi, ma dal problema che il software deve rappresentare. Un'introduzione ai principi fondamentali del DDD: Ubiquitous Language, Bounded Context, Entity, Value Object, Aggregate, Domain Event e isolamento del dominio dall'infrastruttura.
Domain-Driven Design: progettare il software partendo dal dominio
Quando iniziamo a progettare un nuovo sistema software siamo naturalmente portati a parlare subito di framework, database, API, microservizi e tecnologie.
Il Domain-Driven Design, spesso abbreviato in DDD, propone di ribaltare questa prospettiva: prima della tecnologia viene il dominio, cioè il problema reale che il software deve comprendere e rappresentare.
DDD non è quindi un framework né un'architettura specifica. È prima di tutto un approccio alla progettazione del software che mette al centro la conoscenza del dominio e la collaborazione tra chi sviluppa il sistema e chi conosce realmente il contesto in cui quel sistema dovrà operare.
Comprendere il dominio prima di progettare la soluzione
Uno degli errori più comuni nella progettazione software consiste nel tradurre troppo rapidamente un problema reale in concetti tecnici.
Si osserva un processo e si pensa subito a tabelle, form, controller, servizi, API o code di messaggi.
Il Domain-Driven Design suggerisce invece di ritardare queste decisioni e di comprendere prima ciò che realmente accade nel dominio.
Occorre individuare gli attori coinvolti, le attività svolte, le decisioni prese, le regole applicate, le eccezioni possibili, gli eventi significativi e le informazioni necessarie per completare i processi.
Questo lavoro richiede un confronto continuo con i Domain Expert, cioè con le persone che conoscono realmente il dominio.
Lo sviluppatore non deve limitarsi a ricevere requisiti da implementare. Deve contribuire alla costruzione di un modello condiviso del problema.
Ubiquitous Language: un linguaggio condiviso
Uno dei concetti fondamentali del Domain-Driven Design è la Ubiquitous Language.
Si tratta di un linguaggio condiviso tra sviluppatori, analisti ed esperti del dominio.
I termini utilizzati per descrivere il business dovrebbero, quando possibile, essere gli stessi utilizzati nel modello software.
Se nel dominio esistono concetti come Ordine, Prenotazione, Paziente, Richiesta, Prestazione o Revisione, il software dovrebbe utilizzare gli stessi termini con lo stesso significato.
In questo modo si riduce la continua traduzione tra linguaggio tecnico e linguaggio del business.
La Ubiquitous Language non è statica.
Man mano che cresce la conoscenza del dominio, alcuni termini possono essere corretti, sostituiti o raffinati.
Il linguaggio e il modello software devono quindi evolvere insieme.
L'obiettivo non è semplicemente rappresentare dati, ma incorporare nel software la conoscenza del dominio.
Il modello come rappresentazione della conoscenza
Nel DDD il modello non deve essere considerato soltanto come una struttura dati.
Un buon modello deve rappresentare concetti, comportamenti, regole e vincoli del dominio.
Questo significa evitare, quando possibile, oggetti che siano semplicemente contenitori di proprietà modificabili liberamente dall'esterno.
Invece di avere soltanto operazioni generiche come:
setStato(...) setCampo(...) update(...)
il modello dovrebbe poter esprimere operazioni significative per il dominio, ad esempio:
ordine.conferma() prenotazione.annulla() richiesta.inviaInRevisione() paziente.fondiCon(...)
In questo modo il codice utilizza lo stesso linguaggio del dominio e le regole diventano più esplicite.
Domain e Subdomain
Un'organizzazione complessa contiene normalmente più aree di conoscenza e responsabilità.
Il dominio complessivo può quindi essere suddiviso in Subdomain.
Non tutte le parti hanno lo stesso valore strategico.
Alcuni subdomain rappresentano ciò che distingue realmente l'organizzazione e costituiscono il Core Domain.
Altri sono necessari ma più generici o di supporto.
Questa distinzione aiuta a decidere dove investire maggiormente nella modellazione e dove invece utilizzare soluzioni più semplici o standardizzate.
Bounded Context
Quando il dominio cresce, difficilmente un unico modello riesce a rappresentarlo correttamente.
Il Domain-Driven Design introduce quindi il concetto di Bounded Context.
Un Bounded Context è un confine all'interno del quale termini, regole e modelli assumono un significato preciso.
La stessa parola può avere significati differenti in contesti differenti.
Il concetto di Cliente, ad esempio, potrebbe assumere caratteristiche diverse nel contesto commerciale, nel contesto amministrativo e nel contesto dell'assistenza.
Non è necessario creare un modello universale che soddisfi contemporaneamente tutti questi significati.
Ogni Bounded Context può possedere il proprio modello coerente.
DDD non significa microservizi
Un errore frequente consiste nell'identificare automaticamente un Bounded Context con un microservizio.
Non sono la stessa cosa.
Il Bounded Context è prima di tutto un confine concettuale e organizzativo del modello.
Può essere implementato come modulo di un monolite, come applicazione indipendente oppure come servizio distribuito.
La scelta tecnologica viene dopo la comprensione del dominio.
Un sistema può quindi applicare perfettamente il Domain-Driven Design senza utilizzare microservizi.
Context Map
I Bounded Context non vivono necessariamente isolati.
Devono spesso scambiarsi informazioni e collaborare.
La Context Map descrive le relazioni tra i diversi contesti.
Queste relazioni possono assumere forme differenti.
Un contesto può esporre un'API, due contesti possono condividere una piccola parte del modello, oppure un contesto può introdurre uno strato di protezione per evitare che il modello di un sistema esterno condizioni il proprio.
L'obiettivo è rendere esplicite le dipendenze invece di lasciarle emergere accidentalmente nel codice.
Anticorruption Layer
Quando un sistema deve integrarsi con un altro sistema che utilizza concetti o modelli differenti, il DDD propone il pattern Anticorruption Layer.
Questo strato traduce il modello esterno nel linguaggio del proprio Bounded Context.
In questo modo il dominio interno non viene contaminato dalle strutture, dalle terminologie o dalle decisioni progettuali del sistema esterno.
È un principio particolarmente utile nelle integrazioni con sistemi legacy, prodotti di terze parti o piattaforme appartenenti ad altre organizzazioni.
Entity
Una Entity è un oggetto del dominio caratterizzato principalmente dalla propria identità.
Due Entity possono avere gli stessi valori e continuare a rappresentare oggetti differenti.
Un ordine, un paziente, una pratica o una prenotazione possono cambiare molti dei propri attributi nel tempo continuando però a rappresentare la stessa entità.
L'identità accompagna quindi l'oggetto durante il suo ciclo di vita.
Value Object
Un Value Object è invece definito dai propri valori e non da un'identità.
Due Value Object con gli stessi valori possono essere considerati equivalenti.
Esempi tipici possono essere un intervallo temporale, una quantità, un indirizzo o un valore monetario, a seconda del dominio.
Quando possibile, i Value Object sono immutabili.
L'immutabilità rende il modello più semplice da comprendere, utilizzare e testare.
Aggregate e Aggregate Root
Un Aggregate è un insieme di oggetti del dominio gestito come una singola unità di consistenza.
All'interno dell'Aggregate esiste un oggetto principale chiamato Aggregate Root.
Le modifiche agli oggetti interni dovrebbero passare attraverso l'Aggregate Root.
Questo permette di proteggere le invarianti del dominio.
Un'invariante è una regola che deve essere sempre rispettata affinché lo stato del sistema sia valido.
L'Aggregate non serve quindi semplicemente a raggruppare oggetti.
Serve soprattutto a stabilire un confine entro il quale garantire la consistenza delle regole del dominio.
Proteggere le invarianti
Un modello dovrebbe impedire la creazione di stati che non hanno significato per il dominio.
Se una richiesta può essere pubblicata soltanto dopo una revisione, il modello dovrebbe impedire il passaggio diretto da bozza a pubblicato.
Se una prenotazione annullata non può essere nuovamente confermata, questa regola dovrebbe essere protetta dal modello.
L'obiettivo è spostare le regole vicino agli oggetti che possiedono la conoscenza necessaria per applicarle.
Tell, Don't Ask
Questo approccio porta naturalmente al principio Tell, Don't Ask.
Invece di interrogare continuamente un oggetto, estrarre il suo stato, prendere una decisione all'esterno e poi modificarlo, è preferibile chiedere all'oggetto di eseguire un'operazione significativa.
Invece di:
leggere stato verificare stato modificare stato
si preferisce qualcosa come:
richiesta.pubblica()
L'oggetto stesso verifica se l'operazione è consentita e protegge le proprie invarianti.
Domain Service
Non tutte le operazioni del dominio appartengono naturalmente a una Entity o a un Value Object.
Quando una responsabilità rappresenta realmente un comportamento del dominio ma non può essere assegnata correttamente a un singolo oggetto, può essere introdotto un Domain Service.
Un Domain Service non dovrebbe però diventare un contenitore generico nel quale spostare tutta la logica.
Deve rappresentare un concetto o un'operazione realmente appartenente al dominio.
Repository
Il Repository rappresenta l'astrazione attraverso cui il dominio recupera e conserva gli Aggregate.
Il dominio non dovrebbe sapere se i dati vengono memorizzati in PostgreSQL, Oracle, MongoDB, un file o un servizio remoto.
Il Repository permette quindi di esprimere operazioni in termini vicini al dominio senza introdurre nel modello i dettagli della persistenza.
Factory
La creazione di un oggetto del dominio può diventare complessa.
Quando la costruzione di una Entity o di un Aggregate richiede diverse regole, verifiche o dipendenze, può essere utile introdurre una Factory.
La Factory incapsula il processo di creazione e garantisce che l'oggetto venga prodotto già in uno stato valido.
Domain Event
Un Domain Event rappresenta qualcosa di significativo che è già avvenuto nel dominio.
Esempi potrebbero essere:
OrdineConfermato PrenotazioneAnnullata PazienteRegistrato RichiestaPubblicata
Un Domain Event descrive un fatto del dominio, non semplicemente un dettaglio tecnico.
Gli eventi possono consentire ad altre parti del sistema di reagire senza introdurre un accoppiamento diretto tra gli oggetti coinvolti.
Domain Event non significa necessariamente messaging
La presenza di Domain Event non obbliga a utilizzare un message broker.
Un evento del dominio può essere gestito anche internamente alla stessa applicazione.
Soltanto quando esistono reali esigenze di integrazione, asincronia, affidabilità o distribuzione può essere opportuno introdurre infrastrutture di messaging.
Anche in questo caso la tecnologia dovrebbe essere una conseguenza del requisito e non il punto di partenza.
Separare dominio e infrastruttura
Uno dei principi più importanti del DDD consiste nel mantenere il dominio il più possibile indipendente dalla tecnologia.
Database, framework, API, interfacce utente e sistemi esterni sono necessari, ma non dovrebbero determinare la struttura del modello.
Il dominio rappresenta la conoscenza e le regole dell'organizzazione.
L'infrastruttura deve supportarlo.
Architettura esagonale
L'Hexagonal Architecture, o Ports and Adapters, è particolarmente coerente con questa impostazione.
Il dominio rimane al centro.
Le interazioni con il mondo esterno avvengono attraverso porte e adapter.
Una porta descrive ciò di cui il dominio ha bisogno o ciò che rende disponibile.
Un adapter implementa l'integrazione concreta con una determinata tecnologia.
L'interfaccia Web può essere un adapter.
Il database può essere un adapter.
Un sistema esterno può essere un adapter.
Un message broker può essere un adapter.
Il dominio non deve dipendere direttamente da nessuno di essi.
Persistence Ignorance
Da questa separazione deriva il principio della Persistence Ignorance.
Gli oggetti del dominio non dovrebbero essere progettati principalmente in funzione del modo in cui vengono salvati sul database.
La persistenza è necessaria, ma dovrebbe restare un dettaglio infrastrutturale.
Questo permette al modello di rappresentare meglio il problema reale e riduce l'accoppiamento con una particolare tecnologia.
DDD e complessità
Domain-Driven Design non significa introdurre più complessità.
Al contrario, il suo obiettivo è gestire consapevolmente la complessità che appartiene realmente al dominio.
Non tutti i sistemi hanno bisogno dello stesso livello di modellazione.
Per applicazioni semplicemente CRUD o con poche regole di business, un modello sofisticato potrebbe rappresentare un costo inutile.
DDD diventa particolarmente utile quando il dominio contiene regole articolate, concetti ambigui, processi complessi e conoscenza che deve essere resa esplicita nel software.
Evitare l'over-engineering
Entity, Value Object, Aggregate, Repository, Factory e Domain Event non devono essere utilizzati soltanto perché fanno parte del vocabolario DDD.
Ogni astrazione introduce un costo.
Un pattern dovrebbe essere introdotto quando risolve un problema reale del modello.
La qualità di un'architettura non dipende dal numero di pattern presenti.
Dipende dalla capacità del sistema di rappresentare correttamente il dominio e di evolvere insieme ad esso.
Modello e software devono evolvere
La prima rappresentazione di un dominio raramente è quella definitiva.
Nuove conversazioni con gli esperti fanno emergere regole che prima non erano conosciute.
Termini apparentemente equivalenti possono rivelarsi differenti.
Alcuni concetti possono essere eliminati o suddivisi.
Il modello deve quindi essere continuamente raffinato.
Il refactoring non riguarda soltanto il codice.
Nel Domain-Driven Design riguarda anche la conoscenza del dominio.
Il valore del feedback
Una buona progettazione non deve isolarsi dagli utenti alla ricerca di un modello perfetto.
Il feedback reale è parte del processo di apprendimento.
Prototipi, piccoli esperimenti e rilasci progressivi permettono di verificare se il modello rappresenta realmente il problema.
L'obiettivo non è costruire immediatamente l'architettura perfetta, ma migliorare continuamente la comprensione del dominio.
DDD come processo continuo
Il Domain-Driven Design può essere sintetizzato come un processo continuo:
comprendere → nominare → modellare → isolare → verificare → evolvere.
Comprendere significa studiare il problema reale.
Nominare significa costruire una Ubiquitous Language.
Modellare significa trasformare la conoscenza in concetti e comportamenti coerenti.
Isolare significa definire confini chiari tra domini, contesti e infrastruttura.
Verificare significa confrontare continuamente il modello con la realtà.
Evolvere significa accettare che il modello debba cambiare insieme alla comprensione del dominio.
Conclusione
Il Domain-Driven Design non è una collezione di pattern da applicare meccanicamente.
È soprattutto un modo di mettere la conoscenza del dominio al centro delle decisioni progettuali.
Framework, database, API, microservizi e tecnologie rimangono strumenti importanti, ma vengono scelti dopo aver compreso il problema da risolvere.
Quando il dominio è complesso, questa inversione di prospettiva può fare una differenza sostanziale.
Il software smette di essere soltanto una struttura tecnica che memorizza e trasferisce dati e diventa una rappresentazione esplicita della conoscenza, delle regole e dei processi dell'organizzazione.
Fonti e riferimenti
JavaScript Domain-Driven Design, Philipp Fehre, Packt Publishing, 2015.
