La serie sul pattern architetturale Command Query Responsibility Segregation (CQRS) continua con l’introduzione al framework Axon, implementazione Java di tale pattern. In questo articolo verrà spiegata l’architettura del framework ed entreremo nel dettaglio di alcune delle sue caratteristiche principali.
Introduzione
Dopo aver preso visione dei concetti del pattern architetturale Command Query Responsibilty Segregation (CQRS), passiamo dalla teoria alla pratica e andiamo a conoscere in dettaglio il framework Axon [1]. Axon rappresenta l’implementazione Java di CQRS. In questo e nei prossimi articoli vedremo concretizzati tutti i concetti teorici illustrati nei due precedenti [2] [3]. Se non lo aveste già fatto, consiglio di leggere preliminarmente i due articoli appena citati per una migliore comprensione di quello corrente. Nei prossimi articoli introdurremo anche degli esempi pratici provenienti da progetti effettivi.
La versione di Axon a cui si fa riferimento in questa serie è la 1.4 (l’ultima stabile al momento della scrittura del presente articolo). È liberamente scaricabile dal sito ufficiale [1] ed è rilasciata con licenza Apache 2.0 [4].
Architettura di Axon
Axon è un lightweight framework pensato per facilitare l’applicazione del pattern architetturale CQRS alle web application, in modo da risolvere (o quantomeno ridurre) i problemi di scalabilità, prestazioni, complessità e concorrenza a cui si faceva riferimento nel precedente articolo di questa serie [3]. L’obbiettivo che si sono posti di raggiungere Allard Buijze e Jettro Coenradie (i due autori di Axon), era quello di fornire l’implementazione di tutti i building blocks di CQRS, in modo da facilitare il lavoro agli analisti e agli sviluppatori, i quali possono così concentrarsi esclusivamente sulla business logic. Axon comunque non nasconde completamente lo strato CQRS: è sempre possibile in ogni caso intervenire anche su questo layer in caso di necessità specifiche. Il fatto che Axon sia lightweight implica che l’implementazione di una applicazione CQRS-based con tale framework non preclude l’utilizzo di altri framework (Spring, Hibernate, iBatis, etc.).
Come abbiamo visto negli articoli precedenti, CQRS è un pattern concettualmente molto semplice. In sintesi si può dire che CQRS non fa altro che separare nettamente la parte applicativa che processa i commands da quella che processa le queries. Abbiamo però già notato che questo concetto, nonostante sia molto semplice, porta con se’ una grossa quantità di caratteristiche potenzialmente molto utili, soprattutto se combinate con l’utilizzo di altri pattern. Axon mette già a disposizione degli utilizzatori i building blocks necessari per implementare facilmente tali pattern. In figura 1 possiamo vedere il diagramma architetturale completo di una web application basata su CQRS.
Figura 1 – Architettura di una web application basata su CQRS.
I blocchi logici di Axon
Come si evince da tale diagramma, i componenti della User Interface interagiscono con il resto dell’applicazione in due modi: inviano commands tramite il command handler e richiedono informazioni tramite il thin data layer. Axon concettualmente può essere pensato come costituito da quattro macro blocchi logici:
- Command Handling
- Domain Modeling
- Repositories and Event Stores
- Event Processing
In questo e nei prossimi articoli entreremo nel dettaglio di tutti e quattro questi blocchi presentando anche degli esempi pratici.
Il thin data layer che si trova fra la User Interface e i data source in genere restituisce soltanto Data Transfer Object (DTO) contenenti il risultato delle query. I contenuti di tali DTO dipendono dalle necessità dello User Interface layer. Nella maggior parte dei casi si tratta semplicemente di un mapping di dati specifico per una view. Axon non fornisce alcun building block per questa parte dell’applicazione. Il motivo di tale scelta è semplice: questo strato non differisce da quello equivalente per una web application classica.
Command Handling
I commands sono rappresentati da oggetti semplici che contengono tutti i dati necessari affinche’ il command handler possa eseguirli. In generale, il nome della classe Java che viene utilizzata esprime l’azione che deve essere fatta e i campi del command forniscono tutte le informazioni necessarie per farla. Il Command Bus riceve i comandi e li indirizza ai Command Handler. Ciascun Command Handler risponde a uno specifico tipo di comando ed esegue una certa parte di business logic in base al contenuto del command. In alcuni casi, tuttavia, si potrebbe anche avere la necessità di voler eseguire anche qualcos’altro (per esempio, validazione, logging, authorization, etc.) indipendentemente dal tipo specifico di comando. Axon fornisce i building blocks per poter implementare un’infrastruttura di command handling con queste caratteristiche.
I vantaggi implicati
Il ricorso a un meccanismo di tipo esplicito per il dispatch dei commands comporta diversi vantaggi. Innanzitutto esiste un unico oggetto che descrive in maniera chiara ed univoca una intenzione da parte di un client. Tenendo traccia di un command si possono conservare sia l’intenzione che i dati relativi ad essa e poterli quindi utilizzare in qualsiasi momento. Con il command handling è facile, tramite web services, esporre i componenti per il command processing anche a client remoti . L’attività di testing risulta facilitata, in quanto la definizione dei test scripts consiste solamente nella definizione della situazione di partenza, del command da eseguire e i risultati attesi a partire in termini di soli commands e events (come vedremo in uno dei prossimi articoli). Ma il vantaggio più grande è che risulta possibile passare agevolmente dalla modalità di command processing sincrona a quella asincrona.
In dettaglio…
Passiamo adesso ai dettagli, partendo dalla creazione di un Command Handler. Abbiamo già detto che un command in Axon può essere implementato con qualsiasi oggetto; non esiste un tipo predefinito che deve essere implementato. Invece il Command Handler deve implementare l’interfaccia org.axonframework.commandhandling.CommandHandler. Questa interfaccia dichiara il solo metodo
Object handle(T command, UnitOfWork unitOfWork) throws Throwable
Axon fornisce delle annotation che consentono di utilizzare come Command Handler anche semplici POJO (Plain Old Java Object). Basta semplicemente aggiungere l’annotation @CommandHandler ai metodi del POJO per farli diventate Command Handler. I metodi così annotati devono dichiarare il command da processare come primo parametro e possono dichiarare un secondo parametro opzionale che rappresenta la UnitOfWork per tale command. Può esistere un solo handler per ogni command type: questa restrizione vale per tutti gli handler registrati per uno stesso Command Bus. Tramite l’adapter
org.axonframework.commandhandling.annotation.AnnotationCommandHandlerAdapter
è possibile trasformare le classi annotate con @CommandHandler in implementazioni di CommandHandler. Il costruttore della classe AnnotationCommandHandlerAdapter prevede un’istanza di CommandBus. Tutti gli annotated handler vanno registrati nel CommandBus tramite il metodo subscribe() dell’adapter usando il command type corretto.
Metodi
Il CommandBus prevede due metodi per l’invio di commands ai rispettivi handlers:
void dispatch(Object command)
e
void dispatch(Object command, CommandCallback callback)
Il primo parametro comune ad entrambi è il command da inviare. Il secondo parametro opzionale è una callback che consente di notificare al component che invia quando il command handling è stato completato. L’interfaccia
org.axonframework.commandhandling.CommandCallback
prevede due metodi:
void onSuccess(R result)
e
void onFailure(Throwable cause)
i quali vengono invocati, rispettivamente, quando il command handling è terminato correttamente o quando si è verificata una eccezione.
Non è detto che una callback venga invocata nello stesso thread che ha inviato il command. Se il thread chiamante necessita del risultato prima di poter continuare la propria esecuzione (anche se gli autori di Axon sconsigliano tale pratica), bisogna usare una callback di tipo
org.axonframework.commandhandling.callbacks.FutureCallback
(che, in pratica, è una combinazione tra la java.util.concurrent.Future e la CommandCallback di Axon).
Attenti alla scalabilità
La necessità di attendere il risultato dell’invio di un command va a scapito della scalabilità dell’applicazione. Una best practice è quella di privilegiare un alto grado di scalabilità e di utilizzare il metodo dispatch che prevede un solo argomento. Nei casi in cui tuttavia attendere l’esito di un command è più importante della scalabilità, in alternativa alla FutureCallback si può usare la classe
org.axonframework.commandhandling.template.CommandTemplate
CommandTemplate funge da wrapper al CommandBus e consente operazioni di tipo “send and wait” che possono essere eseguite tramite una single method call. La classe CommandTemplate può essere utilizzata in maniera sicura in un ambiente multi-threaded. Per alcuni metodi è anche possibile definire un timeout. Viene lanciata un’eccezione se non si riceve alcuna risposta entro la fine del periodo di timeout.
I meccanismi del Command Bus
Il Command Bus è il meccanismo che inoltra i command ai rispettivi handler. I command vengono inviati a un solo Command Handler. Nel caso in cui nessun Command Handler sia disponibile nel momento in cui viene inoltrato un command, viene lanciata una eccezione di tipo
org.axonframework.commandhandling.NoHandlerForCommandException
Qualora si tenti di registrare più Command Handler per lo stesso command type, ogni nuova registrazione sovrascrive la precedente. Axon fornisce una sola implementazione dell’interfaccia CommandBus:
org.axonframework.commandhandling.SimpleCommandBus
Tramite tale classe è possibile registrare e deregistrare i command handler usando rispettivamente i metodi
public void subscribe(Class commandType, CommandHandler<? super T> handler)
e
public void unsubscribe(Class commandType, CommandHandler<? super T> handler)
EventBus
Uno dei vantaggi derivanti dall’utilizzo dell’EventBus è la possibilità di eseguire azioni basate su tutti i command in arrivo, non necessariamente legati a un tipo di comando. Ciò è possibile tramite i Command Handler Interceptors. Questi possono intraprendere un’azione sia prima che dopo un command processing. Essi possono anche bloccare completamente un command processing. Gli interceptor devono implementare l’interfaccia
org.axonframework.commandhandling.CommandHandlerInterceptor
Tale interfaccia dichiara il solo metodo:
Object handle(Object command, UnitOfWork unitOfWork, InterceptorChain interceptorChain) throws Throwable
il quale prevede tre argomenti, il command, la Unit of Work corrente e l’istanza di una classe che implementa l’interfaccia
org.axonframework.commandhandling.InterceptorChain
Quest’ultima rappresenta la catena di altri interceptor che consentono a questo interceptor di continuare il command processing.
Unit of Work
Un altro concetto importante in Axon e che abbiamo già nominato diverse volte nel paragrafo precedente è la cosiddetta Unit of Work. Il processing di un command può essere pensato come una singola unità. Ogni volta che un command handler esegue una action, questa viene tracciata nell’attuale Unit of Work. Non appena il command handling è terminato, la Unit of Work viene committata e tutte le action finalizzate. In questo modo ogni repository riceve una notifica dei cambiamenti di stato e inoltre vengono inviati all’Event Bus degli eventi schedulati per la pubblicazione. Le Unit of Work servono per due scopi:
- fungono da interfaccia verso i repository, rendendo la vita più facile perchè non si ha necessità di salvare in maniera esplicita i cambiamenti;
- sono un riferimento importante per gli interceptors per scoprire cosa ha fatto un command handler.
Le Unit of Work vengono usate principalmente dai building blocks che Axon mette a disposizione ed è quindi raro che si abbia necessità di accedere ad esse, ma è possibile comunque farlo nei casi in cui non se ne possa proprio fare a meno.
I command handler possono lanciare una eccezione come risultato di un command processing. Se non specificato diversamente, queste eccezioni causano un rollback dei cambiamenti da parte della Unit of Work. Di conseguenza nessun evento viene memorizzato o pubblicato. In alcuni casi si potrebbe però avere necessità di committare ugualmente una Unit of Work e notificare tramite callback al dispatcher del comando che si è verificata una eccezione. La classe SimpleCommandBus consente di indicare una istanza di un oggetto che implementa l’interfaccia
org.axonframework.commandhandling.RollbackConfiguration
Questa serve a indicare all’applicazione come comportasi a fronte di una eccezione (eseguire lo stesso il commit della Unit of Work oppure farne il rollback). Axon prevede già due implementazioni dell’interfaccia RollbackConfiguration. La prima (quella di default) è
org.axonframework.commandhandling.RollbackOnAllExceptionsConfiguration
e provoca un rollback ad ogni eccezione. La seconda è
org.axonframework.commandhandling.RollbackOnUncheckedExceptionConfiguration
questa esegue un commit della Unit of Work a fronte di tutte le checked exception (quelle che non estendono java.lang.RuntimeException).
Conclusioni
In questo articolo abbiamo iniziato a prendere confidenza con l’architettura di Axon e in particolare con il macro blocco del Command Handling. Nei prossimi articoli, a partire da febbraio 2013, vedremo in dettaglio anche gli altri blocchi 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
https://www.mokabyte.it/cms/article.run?articleId=7TK-XI4-MRI-VMN_7f000001_13046033_40989f48
[3] Guglielmo Iozzia, “Command Query Responsibility Segregation pattern II parte: Quando utilizzarlo?”, Mokabyte 178, Novembre 2012
https://www.mokabyte.it/cms/article.run?articleId=AKD-QEF-VCL-OUP_7f000001_13046033_f3c5af0b
[4] Apache License 2.0
http://www.apache.org/licenses/LICENSE-2.0.html