Источники событий - несколько событий или одно для изменения одной совокупности?

Aug 21 2020

У меня есть система контрольных списков, в которой мы внедряем CQRS / ES (Event Sourcing). У нас есть команда

updateStatus(taskId: string, status: boolean)

чтобы отметить задачу или подзадачу как выполненную. Если я получаю команду о том, что подзадача завершена, и все сопутствующие подзадачи также выполнены, я также должен отметить родительскую задачу как выполненную. Итак, в примере ниже (подзадачи 1-3 задачи A):

  • [] задача A - открытая
    • [] задача 1 - открыть
    • [*] задание 2 - выполнено
    • [*] задание 3 - выполнено

Обе задачи A и 1 открыты изначально, а затем я получаю команду

updateStatus(task1, completed)

CommandHandler должен сгенерировать событие taskCompleted (task1).

Мой вопрос в том, что является правильным требованием CQRS / ES:

  • Создать одно событие: taskCompleted (task1)
  • Сгенерируйте два события: taskCompleted (task1), taskCompleted (taskA)

В первом варианте я ожидал бы, что потребители событий увидят, что агрегат также должен обновиться для завершения. Во втором - дескриптор команды.

Главный недостаток варианта 1 - это больше обработки для обработчиков команд и их более глубокое знание агрегата. Другим недостатком является повторное использование событий (например, у нас есть логика для отправки электронного письма владельцу задачи, когда она будет завершена, с вариантом 2 будет просто второй обработчик событий, который просто слушает события и действует на них, не зная, полная логика).

Главный недостаток варианта 2 - гораздо большее количество событий.

Любые предложения о том, какой подход более правильный с использованием CQRS / ES?

Ответы

5 Andy Aug 21 2020 at 13:30

Краткий ответ: вы должны сгенерировать два события.

Вызов одной команды может привести к нескольким событиям, поэтому создание большего количества из них действительно не проблема. Но почему именно это в вашем случае? Чтобы предотвратить рассыпание ответственности.

Я могу представить себе, что в очень простом проекте с событиями, в вашем приложении есть как минимум две рабочие части:

  1. модели из событийных источников,
  2. проекторы обновляют читаемую сторону вашего приложения, чтобы генерировать данные для чтения.

Если вы сгенерировали только одно событие - что подзадача была завершена, теперь вам нужно будет ввести логику в ваши проекторы, чтобы родительская задача также была завершена после завершения всех подзадач. Вы дублируете логику домена, потому что она также будет существовать на вашем уровне записи / домена, чтобы завершить агрегат родительской задачи после завершения всех подзадач. Вдобавок ко всему, вполне вероятно, что такая логика будет написана на совершенно другом языке, чем ваш домен, например, на SQL, если ваши модели чтения находятся в базе данных SQL.

Если ваше приложение находится на стадии, которую я описал (т.е. сторона записи с проекторами стороны чтения), вы можете сказать, что дублирование логики предметной области на самом деле не проблема. В конце концов, во многих проектах реализация SQL также может включать правила домена. Проблема становится более очевидной, когда ваше приложение растет и / или, возможно, даже разделяется между микросервисами.

Если вы добавите микросервис уведомлений, который должен уведомлять всех наблюдателей о задаче, когда задача завершена, с помощью одного события (завершения подзадачи) ваш способ определения завершенности задачи снова скопирует логику домена задачи - проверяя ее локальную базу данных, все ли подзадачи уже выполнены. Что делает это еще более сложным, в отличие от проекторов, этот микросервис, скорее всего, будет жить в совершенно другом проекте, помимо проекта микросервиса, содержащего управление задачами. Это чрезвычайно затрудняет отслеживание неработающей логики домена, которая не разбросана по всей вашей инфраструктуре.

С двумя событиями отметить родительскую задачу в проекторе так же просто, как сделать:

fun changeTaskToCompleted(event: TaskCompletedEvent) {
    database.executeUpdate('UPDATE task SET completed = true WHERE id = ?', event.taskId)
}

а в вашем микросервисе уведомлений реализация также значительно упрощена за счет реагирования только на TaskCompletedEvent:

fun processEvent(event: Event) {
    when(event) {
        is TaskCompletedEvent -> sendTaskCompletedNotificationEmail(event)
    }
}
2 BartvanIngenSchenau Aug 21 2020 at 14:22

В дополнение к вопросам, поднятым в ответе @Andy , если у вас есть два события, вы можете организовать свой код так, чтобы проверка того, выполнены ли все одноуровневые задачи, перемещалась в обработчик событий.

Это сделало бы поток действий

  1. Обработчик команд получает updateStatus(task1, completed)
  2. Обработчик команд генерирует событие taskCompleted(task1)
  3. Обработчик события TaskCompleted получает событие для Task1
  4. Обработчик событий видит, что все родственные задачи выполнены
    • Обработчик событий выдает updateStatus(taskA, completed)команду обработчику команд, или
    • Обработчик событий генерирует событие taskCompleted(taskA)

Таким образом, обработчику команд даже не нужно знать о завершении родительских задач, когда все подзадачи выполнены. Все это обрабатывается в специальном обработчике событий.

afh Aug 22 2020 at 12:42

Главный недостаток варианта 2 - гораздо большее количество событий.

Любые предложения о том, какой подход более правильный с использованием CQRS / ES?

Имея несколько событий для различных вещей , которые произошли в не недостаток , но улучшает конструкцию. Таким образом, логика интерпретации изменения данных для выражения того, что произошло с точки зрения бизнеса, инкапсулируется в вашем сервисе и не просачивается наружу на несколько проекторов. Ответ Энди уже очень хорошо объяснил это.

И, конечно же, вполне нормально генерировать несколько событий после выполнения одной команды . Это деталь реализации, как будут запускаться последующие события.

SubTaskCompleted событие может вызвать некоторые другой код , который проверяет , если все подзадачи из задачи будут завершены в настоящее время , а затем вызвать TaskCompleted события. Но это также может быть в том же методе, который выполняет команду, определяющую оба события, которые должны быть сгенерированы по завершении подзадачи.

Примечание : я бы не запускал последующие события SubTaskCompleted, когда вся основная задача была завершена с отдельным взаимодействием с пользователем, потому что такой прогресс подзадачи больше не представляет интереса, когда вся основная задача была проверена как завершенная. Поскольку события должны отражать то, что на самом деле произошло в системе, если вы помечаете основную задачу как выполненную одним щелчком мыши, с моей точки зрения, не имеет смысла создавать события завершения подзадачи для всех соответствующих подзадач.

Несмотря на то, что ваш вопрос и ответы сильно сосредоточены на событиях (что, конечно, хорошо), я просто хочу указать, что я вижу некоторый потенциальный запах, касающийся вашей команды :

У нас есть команда

updateStatus(taskId: string, status: boolean)

чтобы отметить задачу или подзадачу как выполненную.

Я уверен , что UpdateStatus никак не отражает ваш деловой язык и , следовательно , не имеет сильного значения , в вашем домене.

Я бы предпочел изменить вашу команду на

completeSubTask(taskId: string)

Это дает вашей команде сильный смысл, который не только лучше выражает бизнес-логику, но и подходит для ваших событий. Кроме того, я часто видел команды / методы, начинающиеся с логического флага, а затем меняющиеся на гораздо большее количество параметров, что усложняет понимание соответствующей бизнес-логики.