CQRS e Event Sourcing: il nostro primo progetto andato in produzione – Parte 1 Write Side

CQRS e Event Sourcing: il nostro primo progetto andato in produzione – Parte 1 Write Side

N.B. Questo contenuto è stato prodotto e pubblicato la prima volta da Flowing, società che da luglio 2022 è confluita in Claranet Italia. Insieme a essa sono confluite riflessioni, temi, metodologie e spunti, ampiamente condivisi e orgogliosamente riproposti all’interno di questo blog. ©claranet

PUBBLICATO IL 08/09/2016 DA

Ricardo

Partner

IN SINTESI

Da circa un anno stiamo sviluppando Soisy, una piattaforma di social lending che ha l’obiettivo di offrire servizi bancari come prestiti, investimenti e pagamenti da privato a privato, senza l’intermediazione di una banca. Abbiamo modellato il dominio del software utilizzando i pattern CQRS e Event Sourcing.

Racconteremo la nostra esperienza in una serie di post a partire da questo.

Cos’è CQRS

CQRS (Command/Query Responsability Segregation) è un pattern utilizzato per la modellazione del dominio di un software.  

Tipicamente ogni azione che viene eseguita in un software può essere definita come comando o come query: il comando è un’operazione che modifica lo stato del sistema, la query è un’operazione di lettura che serve a reperire delle informazioni lasciando il sistema inalterato. Paragonandolo ai verbi definiti in HTTP, un comando potrebbe rappresentare una POST mentre la query una GET.

Se ci pensate, queste due azioni sono concetti ben separati che spesso tendiamo a racchiudere in un unico modello di dominio. CQRS si basa proprio su questa idea: comandi e query dovrebbero essere tenuti separati. Nel nostro dominio ogni modello dovrebbe essere pensato per la lettura o per la scrittura, in scenari complessi infatti, un modello che si occupa sia di scrittura che di lettura potrebbe facilmente crescere a dismisura diventando presto difficile da gestire e quindi costoso. Da qui la scelta di spezzare il modello in due parti distinte e separate: “write side” e “read side”.

Cos’è Event Sourcing

Event Sourcing è un pattern che utilizza gli eventi come strumento per modellare la logica di business. Lo stato dell’applicazione e i suoi cambiamenti vengono rappresentati come una sequenza di eventi salvati nella base di dati, chiamata event store. Non esistono operazioni di modifica o cancellazione di eventi, siamo in append mode e tutto quello che è successo nel nostro dominio è rappresentato da questa sequenza. Pensiamo, ad esempio, a cosa succede nella realtà quando commettiamo un errore: non possiamo cancellare l’errore commesso ma possiamo compiere un’azione per porvi rimedio. L’azione compiuta risolve l’errore ma non lo cancella dal tempo: questo è quello che succede all’interno di un dominio modellato ad eventi.

Differentemente da un comando che rappresenta un azione che si sta compiendo nel presente, come ad esempio “approve a loan”, l’evento rappresenta un’azione passata ed è descritta come tale, come ad esempio “loan was approved”.

Andiamo un po’ più in profondità per cercare di capire meglio quali sono le componenti coinvolte e come implementare la modellazione del nostro dominio tramite l’unione di questi due pattern.

Quella che vedrete è un implementazione di cqrs + event sourcing realizzata tramire l’utilizzo di broadway, ma non l’unica possibile. Broadway è una libreria PHP che mette a disposizioni delle componenti utili ad implementare questi pattern.

Comando

Il comando rappresenta un’azione che viene eseguita sul sistema: in OOP può essere rappresentato come un oggetto il cui nome descrive l’azione e il suo stato rappresenta tutta l’informazione necessaria a compiere quell’azione.

class ApproveLoan{   private $loanId;   private $approvedAt;   public function __construct(LoanId $loanId, \Datetime $approvedAt)   {       $this->loanId = $loanId;       $this->approvedAt = $approvedAt;   }   public function getLoanId()   {       return $this->loanId;   }   public function getApprovedAt()   {       return $this->approvedAt;   }}

Il comando è dunque il messaggio inviato all’aggregato radice ( Aggregate Root – AR) e contiene i dati necessari ad eseguire la logica di business rappresentata dall’azione.

Nella nostra applicazione, abbiamo implementato i comandi come oggetti immutabili per renderne più sicuro l’utilizzo.

Evento

L’evento rappresenta una cosa successa nel nostro sistema: in OOP può essere rappresentato come un oggetto immutabile il cui nome descrive l’azione successa e il suo stato rappresenta il nuovo stato persistito.

class LoanApproved{   private $loanId;   private $approvedAt;   private $status;   public function __construct(LoanId $loanId, $status, \Datetime $approvedAt)   {       $this->loanId = $loanId;       $this->approvedAt = $approvedAt;       $this->status = $status;   }   public function getLoanId()   {       return $this->loanId;   }   public function getApprovedAt()   {       return $this->approvedAt;   }   public function getStatus()   {       return $this->status;   }   public static function deserialize(array $data)   {...}   public function serialize()   {...}}

I metodi serialize e deserialize servono rispettivamente a preparare il dato ad essere salvato e per idratare l’oggetto per essere utilizzato.

Come vedremo tra poco, ad ogni comando inviato all’aggregato radice, viene emesso e salvato un evento che rappresenta il cambiamento di stato dovuto all’azione richiesta.

Aggregato

Un aggregato rappresenta il cuore del nostro dominio o di una parte di esso. Può essere visto come un grafo di oggetti (o anche un unico oggetto) che definisce logiche di business. Generalmente si individua un singolo oggetto del grafo, detto aggregato root, che ne espone tutto il comportamento. L’aggregato root è l’unico punto d’accesso all’intero aggregato.

Questo oggetto ha la responsabilità di ricevere il comando, eseguire logica di business  rispettando le invarianti e infine generare eventi che verrano poi salvati sull’event store. Le invarianti rappresentano dei vincoli di dominio che impedisco all’aggregato di assumere degli stati inconsistenti ovvero stati che non esistono nel dominio e che quindi non dovrebbero esistere nell’aggregato. 

Lo stato di un aggregato viene sempre ricostruito dallo stream dei suoi eventi. Ogni evento ha sempre un ID che rappresenta l’identità del nostro aggregato (loanId). La sequenza di eventi con lo stesso ID rappresentano la storia del nostro aggregato e quindi tutti i suoi stati da quando è stato creato ad oggi.

class Loan extends EventSourcedAggregateRoot{   private $loanId;   private $approvedAt;   …   public function approve(\DateTime $approvedAt)   {       if ($this->approvedAt) { //esempio di invariante           throw new LoanAlreadyApproved();       }       $status  = … calcola status (OK/KO)… ; //logica di business       $this->apply( //generazione evento           new LoanApproved(               $this->loanId,               $status,               $approvedAt               )       )   }   protected function applyLoanApproved(LoanApproved $event)   {       $this->approvedAt = $event->getApprovedAt();   }    ....}

Nell’esempio sopra, l’aggregato root Loan espone un metodo approve che riceve le informazioni dal comando ApproveLoan (poi vedremo come), controlla che il Loan non sia già stato approvato, esegue della logica per calcolare lo status ed infine “applica” l’evento LoanApproved che verrà successivamente salvato.

L’apply esegue il metodo applyLoanApproved, che riceve l’evento appena creato e lo utilizza per aggiornare lo stato dell’aggregato. Come vedremo nel prossimo paragrafo, questo metodo viene utilizzato anche per ricostruire lo stato dell’aggregato.

Nell’aggregato troverete sempre due tipologie di metodi: applyNOME_EVENTO che ha la sola responsabilità di aggiornare lo stato dell’aggregato, e metodi pubblici come “approve” che si occupano esclusivamente di eseguire logica di business. Evitate di inserire della logica all’interno dei vari applyNOME_EVENTO, dato che renderebbe più difficile fare test, fare debug e complicherebbe la ricostruzione dell’aggregato.

A questo punto potreste avere un dubbio: “per ogni evento che viene applicato all’interno dell’aggregato, devo scrivere il rispettivo metodo applyNOME_EVENTO ?”

La risposta è no. Come abbiamo detto, questi metodi servono per aggiornare lo stato e quindi vanno aggiornate solo le proprietà utili alla logica di business. Nell’esempio sopra, viene aggiornata solo la proprietà approvedAt che serve per capire se un prestito è già stato approvato.

Introduciamo ora un altro componente che abbiamo utilizzato per permettere di ricostruire l’aggregato e inviare il comando ApproveLoan: il Command Handler.

Command Handler

Il Command Handler non è un oggetto di dominio ed esegue queste tre azioni:

(1) ricostruisce lo stato dell’aggregato portandolo a quello corrente,

(2) invia il comando all’aggregato tramite l’aggregato root,

(3) persiste gli eventi generati dall’aggregato.

class CommandHandler extends \Broadway\CommandHandling\CommandHandler{   private $repository;   public function __construct(EventSourceRepository $repository)   {       $this->repository = $repository;   }   ….   protected function handleApproveLoan(ApproveLoan $command)   {       $loan = $this->repository->load($command->getLoanId()); (1)       $loan->approve($command->getApprovedAt()); (2)       $this->repository->save($loan); (3)   }}

Il metodo load ricostruisce l’aggregato applicando tutto lo stream dei suoi eventi. Se ad esempio nello stream c’è l’evento LoanWasRequested, verrà chiamato il metodo protetto dell’aggregato root applyLoanWasRequested che andrà appunto ad assegnare i dati relativi della richiesta di prestito.

//dentro l’aggregato root Loanprotected function applyLoanWasRequested(LoanWasRequested $event){   $this->loanId = $event->getLoanId();   $this->instalmentsNumber = $event->getInstalmentsNumber();   $this->amount = $event->getAmount();   ...   $this->requestedAt = $event->getRequestedAt();}

Dopo avere riportato l’aggregato al suo stato corrente, viene chiamato il metodo approve che genera l’evento LoanApproved il quale viene salvato nell’event store tramite il metodo save.
A questo punto abbiamo completato la nostra “write side”.

Nel prossimo post vedremo come implementare la “read side”.


Contatta i nostri esperti

per parlare di come possiamo aiutare te e la tua azienda ad evolvere

Contattaci