Suministro de eventos: ¿varios eventos o uno solo para un cambio en un agregado?
Tengo un sistema de lista de verificación donde estamos implementando CQRS / ES (Event Sourcing). Tenemos un comando
updateStatus(taskId: string, status: boolean)
para marcar una tarea o subtarea como completada. Si recibo un comando de que se completa una subtarea, y todas las subtareas de hermanos también se completan, también tengo que marcar la tarea principal como completada. Entonces, en el siguiente ejemplo (subtareas 1-3 de la tarea A):
- [] tarea A - abierta
- [] tarea 1 - abrir
- [*] tarea 2 - completada
- [*] tarea 3 - completada
Las tareas A y 1 están abiertas inicialmente y luego recibo un comando
updateStatus(task1, completed)
CommandHandler necesita generar un evento taskCompleted (task1).
Mi pregunta es cuál es el requisito correcto de CQRS / ES:
- Genere un solo evento: taskCompleted (task1)
- Genere dos eventos: taskCompleted (task1), taskCompleted (taskA)
En la primera opción, esperaría que los consumidores de eventos vean que el agregado también debe actualizarse para completarse. En el segundo, el mando de control se encarga de ello.
La principal desventaja de la opción 1 es un mayor procesamiento para los controladores de comandos y su conocimiento más profundo del agregado. Otra desventaja es la reutilización de los eventos (por ejemplo, digamos que tenemos lógica para enviar un correo electrónico al propietario de la tarea cuando se completa, con la opción 2 simplemente habría un segundo controlador de eventos que solo escucha los eventos y actúa sobre ellos sin conocer el lógica completa).
La principal desventaja de la opción 2 es un número mucho mayor de eventos.
¿Alguna sugerencia sobre cuál es el enfoque más correcto utilizando CQRS / ES?
Respuestas
Respuesta corta: debe generar dos eventos.
La invocación de un solo comando puede generar varios eventos, por lo que generar más de ellos no es un problema. Pero, ¿por qué exactamente querrías hacer eso en tu caso? Para evitar la dispersión de responsabilidades.
En un proyecto de origen de eventos muy básico, puedo imaginar que hay al menos dos partes funcionales en su aplicación:
- modelos de eventos,
- proyectores que actualizan el lado de lectura de su aplicación para generar datos para lectura.
Si generó solo un evento: que se completó una subtarea, ahora deberá introducir la lógica en sus proyectores, para que una tarea principal también se complete al completar todas las subtareas. Está duplicando la lógica del dominio, porque la misma también vivirá en su capa de escritura / dominio, para completar el agregado de la tarea principal al completar todas las subtareas. Además de eso, es muy probable que dicha lógica esté escrita en un lenguaje completamente diferente al de su dominio, por ejemplo, en SQL si sus modelos de lectura están en una base de datos SQL.
Si su aplicación se encuentra en la etapa que he descrito (es decir, el lado de escritura con proyectores del lado de lectura), podría decir que duplicar la lógica del dominio no es realmente un problema. Después de todo, en muchos proyectos una implementación de SQL también puede incluir reglas de dominio. El problema se vuelve más evidente cuando su aplicación crece y / o quizás incluso se divide entre microservicios.
Si agrega un microservicio de notificación que debería notificar a todos los observadores de una tarea cuando se completa la tarea, con un solo evento (de finalización de la subtarea), su forma de determinar la compleción de la tarea copiaría una vez más la lógica del dominio de la tarea, verificando su base de datos local si todo las subtareas ya están completadas. Lo que hace que esto sea aún más complicado, a diferencia de los proyectores, es muy probable que este microservicio viva en un proyecto totalmente diferente, aparte del proyecto de microservicio que contiene la gestión de tareas. Esto hace que sea extremadamente difícil rastrear la lógica de dominio rota, que no está dispersa en toda su infraestructura.
Con dos eventos, marcar una tarea principal en un proyector es tan simple como hacer:
fun changeTaskToCompleted(event: TaskCompletedEvent) {
database.executeUpdate('UPDATE task SET completed = true WHERE id = ?', event.taskId)
}
y en su microservicio de notificación, la implementación también se simplifica enormemente al reaccionar solo a TaskCompletedEvent
:
fun processEvent(event: Event) {
when(event) {
is TaskCompletedEvent -> sendTaskCompletedNotificationEmail(event)
}
}
Además de los puntos planteados en la respuesta de @Andy , si tiene dos eventos, puede organizar su código de manera que la verificación de si se completaron todas las tareas del hermano se mueva a un controlador de eventos.
Esto haría que el flujo de acciones
- El controlador de comandos recibe
updateStatus(task1, completed)
- El controlador de comandos emite un evento
taskCompleted(task1)
- El controlador de eventos TaskCompleted recibe el evento de Task1
- El controlador de eventos ve que se completan todas las tareas del hermano
- El controlador de eventos emite un comando
updateStatus(taskA, completed)
al controlador de comandos, o - El controlador de eventos emite un evento
taskCompleted(taskA)
- El controlador de eventos emite un comando
De esta manera, el controlador de comandos ni siquiera tiene que saber sobre la finalización de las tareas principales cuando se completan todas las subtareas. Todo eso se maneja en un controlador de eventos dedicado.
La principal desventaja de la opción 2 es un número mucho mayor de eventos.
¿Alguna sugerencia sobre cuál es el enfoque más correcto utilizando CQRS / ES?
Tener múltiples eventos para diferentes cosas que sucedieron no es una desventaja, pero mejora su diseño. Con eso la lógica de interpretar el cambio de los datos para expresar lo sucedido desde la perspectiva empresarial se encapsula en su servicio y no se filtra al exterior a varios proyectores. La respuesta de Andy ya lo ha explicado muy bien.
Y, por supuesto, está bien generar varios eventos después de que se haya ejecutado un solo comando . Es un detalle de implementación cómo se activarán los eventos posteriores.
Un evento SubTaskCompleted podría activar algún otro código que verifique si todas las subtareas de la tarea se completan ahora y luego activar el evento TaskCompleted . Pero también podría ser dentro del mismo método que ejecuta el comando que se determinen ambos eventos que deben emitirse debido a la finalización de la subtarea.
Nota : No desencadenaría eventos SubTaskCompleted posteriores cuando toda la tarea principal se completó con una interacción de usuario separada porque el progreso de dicha subtarea ya no es interesante cuando se verificó que se completó una tarea principal completa. Como los eventos deberían reflejar lo que realmente sucedió en el sistema, si marca una tarea principal como completada con un solo clic, no tendría sentido producir eventos de subtarea completados para todas las subtareas correspondientes desde mi punto de vista.
A pesar de su pregunta y las respuestas fuertemente enfocadas en los eventos (lo cual es bueno, por supuesto), solo quiero señalar que veo algún olor potencial con respecto a su comando :
Tenemos un comando
updateStatus(taskId: string, status: boolean)
para marcar una tarea o subtarea como completada.
Estoy bastante seguro de que updateStatus qué no reflejar su lenguaje de negocios y por lo tanto no tiene sentido fuerte de su dominio.
Preferiría sugerirle que cambie su comando a
completeSubTask(taskId: string)
Esto le da a su comando un significado fuerte que no solo expresa mucho mejor la lógica empresarial, sino que también se adapta a sus eventos. Además, a menudo he visto comandos / métodos que comienzan con una bandera booleana y luego se cambian a muchos más parámetros, lo que dificulta cada vez más la comprensión de la lógica empresarial correspondiente.