Approvvigionamento di eventi: più eventi o uno solo per una modifica su un aggregato?
Ho un sistema di liste di controllo in cui stiamo implementando CQRS / ES (Event Sourcing). Abbiamo un comando
updateStatus(taskId: string, status: boolean)
per contrassegnare un'attività o un'attività secondaria come completata. Se ricevo un comando che indica che un'attività secondaria è stata completata e anche tutte le attività secondarie di pari livello sono state completate, devo contrassegnare anche l'attività principale come completata. Quindi nell'esempio seguente (sottoattività 1-3 dell'attività A):
- [] attività A - apri
- [] attività 1 - apri
- [*] attività 2 - completata
- [*] attività 3 - completata
Inizialmente le attività A e 1 sono entrambe aperte, quindi ricevo un comando
updateStatus(task1, completed)
CommandHandler deve generare un evento taskCompleted (task1).
La mia domanda è qual è il requisito CQRS / ES corretto:
- Genera un singolo evento: taskCompleted (task1)
- Genera due eventi: taskCompleted (task1), taskCompleted (taskA)
Nella prima opzione mi aspetto che i consumatori di eventi vedano che anche l'aggregato deve aggiornarsi per essere completato. Nel secondo, l'handle di comando se ne occupa.
Il principale svantaggio dell'opzione 1 è una maggiore elaborazione per i gestori di comandi e la loro conoscenza più approfondita dell'aggregato. Un altro svantaggio è il riutilizzo degli eventi (ad esempio, supponiamo di avere una logica per inviare un'e-mail al proprietario dell'attività quando è completata, con l'opzione 2 ci sarebbe semplicemente un secondo gestore di eventi che ascolta semplicemente gli eventi e agisce su di essi senza conoscere il logica completa).
Il principale svantaggio dell'opzione 2 è un numero molto maggiore di eventi.
Qualche suggerimento su quale sia l'approccio più corretto utilizzando CQRS / ES?
Risposte
Risposta breve: dovresti generare due eventi.
Una singola chiamata di comando può portare a più eventi, quindi generarne di più non è davvero un problema. Ma perché esattamente dovresti farlo nel tuo caso? Per evitare la dispersione di responsabilità.
In un progetto di origine evento molto semplice, posso immaginare che ci siano almeno due parti di lavoro per la tua applicazione:
- modelli provenienti da eventi,
- proiettori che aggiornano il lato di lettura dell'applicazione per generare dati da leggere.
Se hai generato un solo evento, ovvero che un'attività secondaria è stata completata, dovrai ora introdurre la logica ai tuoi proiettori, per completare anche un'attività principale al termine di tutte le attività secondarie. Stai duplicando la logica del dominio, perché la stessa risiederà anche nel tuo livello di scrittura / dominio, per completare l'aggregazione dell'attività principale al completamento di tutte le sottoattività. Inoltre, è molto probabile che tale logica venga scritta in una lingua completamente diversa dal tuo dominio, ad esempio in SQL se i tuoi modelli di lettura si trovano in un database SQL.
Se la tua applicazione è nella fase che ho descritto (cioè lato scrittura con proiettori lato lettura), potresti dire che duplicare la logica del dominio non è davvero un problema. Dopo tutto, in molti progetti un'implementazione SQL può includere anche regole di dominio. Il problema diventa più evidente quando la tua applicazione cresce e / o forse viene addirittura suddivisa tra i microservizi.
Se aggiungi un microservizio di notifica che dovrebbe notificare a tutti i watcher di un'attività quando l'attività è completata, con un singolo evento (di completamento della sottoattività) il tuo modo di determinare la completezza dell'attività copierà ancora una volta la logica del dominio dell'attività, controllando il database locale se tutti le attività secondarie sono già completate. Ciò che lo rende ancora più complicato, a differenza dei proiettori, è molto probabile che questo microservizio risieda in un progetto completamente diverso, a parte il progetto di microservizio contenente la gestione delle attività. Ciò rende estremamente difficile tenere traccia della logica del dominio interrotto, che non è dispersa nell'intera infrastruttura.
Con due eventi, contrassegnare un'attività principale in un proiettore è semplice come eseguire:
fun changeTaskToCompleted(event: TaskCompletedEvent) {
database.executeUpdate('UPDATE task SET completed = true WHERE id = ?', event.taskId)
}
e nel tuo microservizio di notifica l'implementazione è anche notevolmente semplificata reagendo solo a TaskCompletedEvent
:
fun processEvent(event: Event) {
when(event) {
is TaskCompletedEvent -> sendTaskCompletedNotificationEmail(event)
}
}
Oltre ai punti sollevati nella risposta da @ Anddy , se hai due eventi, puoi organizzare il tuo codice in modo tale che il controllo se tutte le attività di pari livello sono state completate viene spostato in un gestore di eventi.
Questo renderebbe il flusso delle azioni
- Il gestore dei comandi riceve
updateStatus(task1, completed)
- Il gestore dei comandi emette un evento
taskCompleted(task1)
- Il gestore dell'evento TaskCompleted riceve l'evento per Task1
- Il gestore eventi vede che tutte le attività di pari livello sono state completate
- Il gestore degli eventi invia il comando
updateStatus(taskA, completed)
al gestore dei comandi o - Il gestore di eventi emette l'evento
taskCompleted(taskA)
- Il gestore degli eventi invia il comando
In questo modo, il gestore dei comandi non deve nemmeno sapere del completamento delle attività principali quando tutte le attività secondarie sono state completate. Tutto questo viene gestito in un gestore di eventi dedicato.
Il principale svantaggio dell'opzione 2 è un numero molto maggiore di eventi.
Qualche suggerimento su quale sia l'approccio più corretto utilizzando CQRS / ES?
Avere più eventi per cose diverse che sono accadute non è uno svantaggio ma migliora il tuo design. Con ciò la logica di interpretare la modifica dei dati per esprimere ciò che è accaduto dal punto di vista aziendale è incapsulata nel tuo servizio e non trapela all'esterno di diversi proiettori. La risposta di Andy lo ha già spiegato molto bene.
E ovviamente va benissimo generare diversi eventi dopo che è stato eseguito un singolo comando . È un dettaglio dell'implementazione come verranno attivati gli eventi successivi.
Un evento SubTaskCompleted potrebbe attivare un altro codice che controlla se tutte le attività secondarie dell'attività sono state completate ora e quindi attivare l' evento TaskCompleted . Ma potrebbe anche essere all'interno dello stesso metodo che esegue il comando che determini entrambi gli eventi che dovrebbero essere emessi a causa del completamento della sotto-attività.
Nota : non attiverei eventi SubTaskCompleted successivi quando l'intera attività principale è stata completata con un'interazione utente separata perché tale avanzamento della sottoattività non è più interessante quando un'intera attività principale è stata controllata come completata. Poiché gli eventi dovrebbero riflettere ciò che è realmente accaduto nel sistema, se contrassegni un'attività principale completata con un solo clic, non avrebbe senso produrre eventi secondari completati per tutte le attività secondarie corrispondenti dal mio punto di vista.
Nonostante la tua domanda e le risposte fortemente incentrate sugli eventi (il che è un bene ovviamente), voglio solo sottolineare che vedo un potenziale odore riguardo al tuo comando :
Abbiamo un comando
updateStatus(taskId: string, status: boolean)
per contrassegnare un'attività o un'attività secondaria come completata.
Sono abbastanza sicuro che updateStatus non non riflettere il vostro linguaggio di business e quindi ha alcun significato forte nel dominio.
Preferirei suggerire di cambiare il tuo comando in
completeSubTask(taskId: string)
Questo dà al tuo comando un significato forte che non solo esprime la logica aziendale molto meglio, ma si adatta anche ai tuoi eventi. Inoltre, ho spesso visto comandi / metodi che iniziano con un flag booleano e successivamente sono stati modificati con molti più parametri, il che rende sempre più difficile la comprensione della logica di business corrispondente.