La serie sul pattern architetturale Command Query Responsibility Segregation (CQRS) continua con l’approfondimento dei macro blocchi che compongono il framework Axon, implementazione Java di tale pattern. In questo articolo è la volta del Domain Modeling.
Nel precedente articolo [4] di questa serie è stata introdotta l’architettura del framework Axon [1] ed è stato illustrato in dettaglio il Command Handling, il primo dei macroblocchi logici che compongono tale framework. In questo (e nel prossimo articolo) andremo a scoprire in dettaglio anche gli altri blocchi partendo dal Domain Modeling. Ci occuperemo quindi nel dettaglio di Axon, ma, come suggerito nella parte precedente, consiglio di leggere preliminarmente anche i due articoli “teorici” su CQRS [2] [3], qualora non lo aveste già fatto, perche’ forniscono una visione d’insieme e consentono una migliore comprensione di questi articoli più “pratici”.
La versione di Axon a cui si fa riferimento è sempre la 1.4.
Domain Modeling
Come anticipato in uno degli articoli precedenti, una web application basata su CQRS avente un dominio complesso può trarre notevoli benefici da un approccio di tipo Domain Driven Design (DDD) [5] [6]. DDD è composto da numerosi building blocks, ma quelli che giocano un ruolo importante in questo caso sono essenzialmente due: Event Sourcing e Aggregate.
Event Sourcing
Axon distingue tra tre tipi di eventi: Domain Events, Application Events e System Events. Tutti e tre devono o implementare l’interfaccia org.axonframework.domain.Event oppure estendere una fra le classi astratte org.axonframework.domain.DomainEvent, org.axonframework.domain.ApplicationEvent e org.axonframework.domain.SystemEvent. Tutti gli eventi possono avere sia dati che metadati. I dati vengono aggiunti a ciascun evento come campi della sua implementazione, mentre i metadati vengono conservati separatamente. Qualsiasi implementazione di evento in Axon consente alle sottoclassi di attaccare informazioni come meta-dati in maniera arbitraria. Una best practice in Axon consiste nel non basare business decisions su informazioni contenute nei metadati degli eventi. Se un certo tipo di informazione è ritenuto importante per tale scopo, allora lo si deve spostare fra i dati reali dell’evento.
Domain Events
Dei tre tipi di evento menzionati in precedenza, il Domain Event è sicuramente quello più importante. Esso rappresenta un evento che si verifica all’interno della logica di dominio, a seguito di un cambiamento di stato o di una notifica speciale da un determinato stato. La classe astratta DomainEvent tiene traccia dell’insieme di eventi che vengono generati e del sequence number di un evento all’interno di tale insieme. Queste informazioni sono importanti per il meccanismo dell’Event Sourcing per conoscere l’origine di un determinato evento. Anche se non è obbligatorio, è sempre bene rendere i domain events immutabili dichiarando tutti i loro campi come final e inizializzando ogni evento all’interno del proprio costruttore.
Sebbene, tecnicamente parlando, un Domain Event sta ad indicare un cambiamento di stato, pensando alla implementazione di un evento di questo tipo si dovrebbe cercare di catturare anche l’intenzione dello stato all’interno dell’evento. Si può realizzare ciò creando una implementazione astratta del domain event per avere la certezza che un determinato stato è cambiato e creare diverse implementazioni concrete (sottoclassi di quella astratta) che permettono di capire anche l’intenzione del cambiamento. In figura 1 un esempio di quanto appena scritto, relativo a una applicazione che gestisce una rubrica:
Figura 1 – Esempio di aggiunta di intenti ad un evento.
In questo esempio la classe astratta AddressChangeEvent è il domain event, mentre le due sottoclassi concrete ContactMovedEvent e AddressCorrectedEvent si occupano della cattura di due intenti del cambiamento di stato di un indirizzo. Gli event listeners che non tengono conto degli intenti legati a un evento (quali ad esempio i listener di eventi relativi all’update di un database) faranno solamente riferimento alla classe astratta, mentre invece quelli che tengono conto degli intenti rimangono in ascolto delle implementazioni concrete dell’evento.
Application Events
Gli Application Events sono eventi che non possono essere classificati come domain events, ma che comunque hanno un significato importante per la web application. Un esempio tipico di Application Event è la scadenza di una sessione utente oppure la notifica dell’invio di una e-mail. In Axon un Application Event deve estendere la classe astratta org.axonframework.domain.ApplicationEvent.
Questa classe genera un identificatore univoco e un timestamp per l’evento corrente. È È possibile (opzionalmente) anche aggiungere un oggetto che funge da source dell’evento: si tenga presente però che tale oggetto è referenziato in maniera debole e questo vuol dire che, ad ogni passaggio del garbage collector o ad ogni serializzazione/deserializzazione dell’evento, la source class originale non sarà più disponibile. Si potrà soltanto accedere al tipo del source e al valore restituito dal suo metodo toString().
System Events
I System Events forniscono notifiche relative allo stato del sistema. Questo tipo di eventi potrebbe, per esempio, indicare che un sottosistema non risponde o che esso ha generato una eccezione. In Axon tutti i System Events devono estendere la classe astratta org.axonframework.domain.SystemEvent. È possibile passare come argomento al costruttore di questo evento un’eccezione (in modo da definire così la causa dell’evento) e un oggetto che funge da source dell’evento (tenendo però presente che per esso vale la stessa considerazione relativa all’accoppiamento debole con l’evento fatta per gli Application Events nel paragrafo precedente).
Aggregate
Per Aggregate si intende un’entità o un insieme di entità che è mantenuto sempre in uno stato consistente. L’oggetto che sta in cima all’albero di queste entità viene indicato come aggregate root ed è responsabile di mantenere consistente lo stato dell’aggregate. Rimanendo nell’ambito dell’applicazione citata nel paragrafo sui Domain Events, un esempio di Aggregate può essere il seguente: un aggregate denominato Contact che comprende due entity, contact e address. Per mantenere l’aggregate in uno stato consistente, ogni volta che si aggiunge un address a un contact, bisogna che tale operazione venga fatta sempre tramite l’entity contact. L’entity contact è quindi la root dell’aggregate. In Axon gli aggregate sono identificati in maniera univoca tramite l’interfaccia org.axonframework.domain.AggregateIdentifier. Il framework fornisce già due implementazioni di tale interfaccia: org.axonframework.domain.UUIDAggregateIdentifier, che lavora tramite la classe java.util.UUID (presente nell’SDK Java a partire dalla release 1.5) per generare UUID in maniera randomica, e org.axonframework.domain.StringAggregateIdentifier, che consente di usare una stringa come identificatore.
Vediamo adesso in dettaglio quali implementazioni di Aggregate sono già presenti in Axon.
Basic aggregate implementation
Tutti gli aggregate root in Axon devono implementare l’interfaccia org.axonframework.domain.AggregateRoot. I metodi di questa interfaccia descrivono tutte le operazioni necessarie a un repository per memorizzare e pubblicare tutti gli eventi che vengono generati. Axon fornisce inoltre anche diverse implementazioni astratte che possono aiutare nella creazione degli aggregate specifici per l’applicazione su cui si sta lavorando. Una di queste è org.axonframework.domain.AbstractAggregateRoot. Essa è l’implementazione più elementare e fornisce il metodo
protected void registerEvent(DomainEvent event)
con il quale è possibile aggiungere eventi ad una lista di uncommited events. AbstractAggregateRoot tiene traccia di tutti gli eventi uncommitted registrati e assicura che essi siano inoltrati all’EventBus nel momento in cui un aggregate viene salvato nel repository.
Event sourced aggregates
Axon fornisce alcune implementazioni di repository che possono ricorrere all’event sourcing come metodo di storage di aggregate. Tali repository richiedono che gli aggregate implementino l’interfaccia org.axonframework.eventsourcing.EventSourcedAggregateRoot. Questa interfaccia extende AggregateRoot e definisce in più solamente il metodo
void initializeState(DomainEventStream domainEventStream)
con il quale si inizializza un aggregate in base ad uno stream di eventi.
Oltre all’interfaccia EventSourcedAggregateRoot, Axon mette a disposizione anche due implementazioni astratte per la creazione di event sourced aggregates. La prima di queste è org.axonframework.eventsourcing.AbstractEventSourcedAggregateRoot. Essa, oltre ad implementare tutti i metodi dell’interfaccia EventSourcedAggregateRoot, definisce il seguente metodo abstract
protected abstract void handle(DomainEvent event)
che bisogna implementare per poter eseguire l’apply di cambiamenti di stato basati su domain events. Estendendo la classe AbstractEventSourcedAggregateRoot, è possibile registrare eventi tramite il metodo
protected void apply(DomainEvent event)
Questo metodo deve registrare un evento che deve essere committed al momento del salvataggio di un aggregate e successivamente deve invocare il metodo handle() passandogli l’evento come parametro.
L’altra classe astratta messa a disposizione da Axon è org.axonframework.eventsourcing.annotation.AbstractAnnotatedAggregateRoot. Essa è una specializzazione di AbstractEventSourcedAggregateRoot e fornisce la annotation @EventHandler per gli aggregate. In questo modo, piuttosto che implementare il singolo metodo handle(), con il rischio che esso possa diventare illeggibile, è possibile spalmare il codice contenuto in esso su più metodi (aventi nomi scelti in maniera arbitraria), annotati con @EventHandler. Sarà compito di AbstractAnnotatedAggregateRoot invocare di volta in volta il metodo corretto. In qualsiasi caso, AbstractAnnotatedAggregateRoot invoca un solo metodo per volta secondo i seguenti criteri:
- prima valuta tutti i metodi annotati con @EventHandler dell’istanza corrente all’interno della gerarchia delle classi;
- nel caso in cui trovi più di un metodo avente un parametro di tipo event, sceglie e quindi invoca il metodo con il parametro di tipo più specifico;
- nel caso in cui non viene trovato alcun metodo al livello corrente della gerarchia delle classi, passa ad esaminare il livello superiore;
- continuando a salire di livello nella gerarchia, quando si raggiunge il livello di AbstractAnnotatedAggregateRoot e non viene trovato nessun event handler, l’evento viene ignorato.
I metodi event handler devono essere mantenuti private: in questo modo si crea una netta separazione fra la parte pubblica di un aggregate, quella che cioè espone i metodi che generano eventi, dalla logica interna che elabora invece gli eventi.
Complex aggregate structures
Applicazioni con una logica applicativa molto complessa spesso richiedono aggregate più complessi di quanto possa essere un aggregate con una sola root. In questi casi è importante distribuire la complessità su più entity all’interno di un aggregate. Quindi non solo la root dell’aggregate necessita di usare gli eventi per innescare transizioni di stato, ma anche tutte le altre entity all’interno dello stesso aggregate. Axon prevede anche questo caso. Tutte le entity di un aggregate devono estendere la classe astratta org.axonframework.eventsourcing.AbstractEventSourcedEntity. Ogni volta che un’entity di un aggregate (inclusa la root) attiva un evento, esso viene registrato nella root. Questa per prima cosa attiva l’evento localmente, quindi valuta tutti i suoi field in cerca di implementazioni di AbstractEventSourcedEntity e infine si occupa dei loro eventi. E la stessa cosa fanno tutte le altre entity dello stesso aggregate con i propri field. Per poter registrare un evento, una entity deve sapere qual è la root dell’aggregate a cui appartiene. Axon registra in maniera automatica la root nelle altre entity prima di attivare eventi in esse. Questo vuol dire che le entity di un aggregate (tranne la root) non hanno un parametro di tipo event nei loro costruttori. Axon individua in maniera automatica la maggior parte delle entity child tra i campi di una entità parent (sia che questa sia root oppure no). Le entità che vengono trovate sono le seguenti:
- quelle referenziate direttamente nei field;
- quelle all’interno di field che implementano l’interfaccia java.lang.Iterable (quindi tutte le collection, come List, Set, etc.);
- quelle all’interno di chiavi e valori di field contenenti implementazioni dell’interfaccia java.util.Map.
Qualora una entity venga referenziata in una location diversa da quelle delle lista precedente, allora bisogna fare l’override del metodo
protected CollectiongetChildEntities()
di AbstractEventSourcedEntity. Esso restituisce una Collection di entity che devono essere notificate all’evento. Ogni entità viene invocata una volta per ogni sua occorrenza all’interno della Collection restituita.
Conclusioni
In questo articolo abbiamo visto in dettaglio il macroblocco del Domain Modeling in Axon. Nei prossimi articoli, vedremo in dettaglio anche gli ultimi due macroblocchi e infine passeremo in rassegna un’applicazione reale CQRS-based in cui viene utilizzata la maggior parte degli strumenti messi a disposizione dal framework.
Riferimenti
[1] Sito ufficiale di Axon framework
[2] Guglielmo Iozzia, “Command Query Responsibility Segregation pattern – I parte: Breve panoramica su CQRS”, MokaByte 177, ottobre 2012
[3] Guglielmo Iozzia, “Command Query Responsibility Segregation pattern – II parte: Quando utilizzarlo?”, MokaByte 178, novembre 2012
[4] Guglielmo Iozzia, “Command Query Responsibility Segregation pattern – III parte: Introduzione ad Axon Framework”, MokaByte 179, dicembre 2012
[5] La voce “Domain Driven Design definition” su Wikipedia
[6] Eric Evans, “Domain-Driven Design: Tackling Complexity in the Heart of Software”, Addison-Wesley, 2003