Обработка событий в отношениях между агрегатами и агрегатном состоянии

Недавно я начал свою первую попытку разработать веб-приложение для продажи билетов, используя принципы проектирования, ориентированные на предметную область, в сочетании с поиском событий и CQRS.

Поскольку это моя первая попытка отойти от традиционного подхода CRUD и перейти в мир DDD, я уверен, что у меня много вещей, спроектированных неправильно, поскольку DDD требует много усилий, чтобы придумать правильное разделение доменов, ограниченных контекстов и т. д.

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

Агрегаты предоставляют действия, которые фактически вызывают события. Например, company.Create(firmName, address, taxid, ...) выдает событие CompanyCreated и применяет его к себе. Когда задание почти завершено, все события из всех агрегатов, загруженных в контексте этого задания, собираются и сохраняются в хранилище событий.

Теперь я попал в ситуацию, которая, я уверен, очень распространена, когда у меня есть отношения между агрегатами. Например, Customer имеет Contacts, или SupportAgent является членом Department. Это агрегаты в моем дизайне.

Возьмем, к примеру, Department. Состояние Department состоит из заголовка, описания, некоторых других свойств и списка идентификаторов SupportAgent тех агентов, которые являются членами этого отдела. Состояние SupportAgent состоит из имени, фамилии, номера телефона, электронной почты, ... и списка идентификаторов Department тех отделов, членом которых является этот агент.

Теперь при обработке команды типа AddAgentToDepartment(agentId, departmentId) выдаются два события. DepartmentAdded выдается для соответствующего агента, который добавит идентификатор отдела в состояние агента, и SupportAgentAdded выдается для соответствующего отдела, который добавит идентификатор агента в состояние отдела.

Мой первый вопрос: Правильно ли сохранять идентификаторы связанных агрегатов в состоянии агрегата? Под «правильным» я подразумеваю, является ли это лучшей практикой? Или есть другой способ (например, поддержание отношений в виде сущности / агрегата «DepartmentMemberManager» или что-то в этом роде. На самом деле эта сущность или что-то еще здесь является чем-то вроде синглтона. Есть ли такая вещь в мире DDD)?

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

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

Если вы эксперт DDD и ES или, по крайней мере, у вас есть опыт, я хотел бы получить несколько подсказок о том, что вы можете увидеть, что я делаю или думаю неправильно, и в каком направлении мне следует смотреть.

4
0
1 761
3

Ответы 3

CQRS означает разделение ответственности между командами и запросами. Есть две стороны C - команда, сторона записи. Q - Запрос, Читать сторону.

Агрегаты находятся на стороне команды C и могут выполнять только команду. Запросы агрегатов невозможны. Итак, в вашем примере обработчик команд вашего агента просто не может разговаривать с некоторым агрегатом отдела

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

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

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

Теперь агрегат - это не сущность. Само название «совокупность» подразумевает, что там есть несколько «вещей». Агрегат - это объект, который может выполнять команды и обеспечивать выполнение бизнес-правил. Это означает, что команда отправляется одному агрегату.

Выбор агрегатов - это основная деятельность по проектированию предметной области в системе CQRS / ES. Ошибки очень дороги, потому что вам придется иметь дело с версией событий и рефакторингом (Грег Янг недавно написал об этом книга)

Итак, в вашем примере у нас есть одна команда:

AddAgentToDepartment(agentId, departmentId)

Первый вопрос - какому агрегату адресовано? Помните - одна команда для одного агрегата. Это дизайнерское решение, которое зависит от вашей системы. Я бы подумал о таких вещах: может ли агент оставаться агентом без этой команды? Думаю, завтра у вас не будет отделов, но, скажем, это не повлияет на продукты и агента. Может ли отдел быть отделом без этой команды? Вряд ли - это группировка агентов. Поэтому я бы сделал отдел совокупностью, которая получает

AddAgentToDepartment(departmentId, params: { agentIdToAdd })

А агрегат отделов будет заботиться о бизнес-правилах (нельзя добавить один и тот же агент дважды, нельзя удалить несуществующего агента и т. д.)

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

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

My first question is: Is it right to keep the ids of related aggregates into the state of an aggregate?

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

My other thought is about event replay. In the previous example, two events are issued, but in order to update the views, only one of them needs to be handled because both events describe the exact same transition in the system's state (an agent and a department are linked).

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

What happens in case we need to replay some events to bring only a certain aggregate's view in a consistent state? Specifically, when I want to replay events for a support agent, only DepartmentAdded events will be replayed, and those events are not handled by anyone, so the views won't be updated. Is it right to replay partially some events or all events in the event store should be replayed in order to bring the whole system into a consistent state?

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

На стороне 'C' - команды (запись) после получения команды состояние агрегата восстанавливается из потока событий этого агрегата путем запроса хранилища событий: дайте мне все события для агрегата 12345.

На стороне Q-запроса (чтение) агрегатов нет, есть модели чтения. Эти модели чтения обычно строятся из событий нескольких типов для разных агрегатов. Когда вам нужно перестроить модель чтения, вы запрашиваете событие store: дайте мне все события, которые соответствуют моим критериям. Затем вы применяете эти события для чтения модели (это может занять некоторое время), и когда модель чтения обновлена, она может подписаться на текущий поток событий и обновить себя в реальном времени.

In my design, I have command handlers that accept a command, initiate a Job (a unit of work) they load the aggregates that are needed from the aggregate repository (which loads aggregates from event store by replaying events), and they manipulate the aggregates through each aggregate's exposed actions and then close the Job.

Скорее всего, вы получите ответный отпор. Изменение нескольких агрегатов в рамках одной транзакции (единицы работы) становится действительно сложным, если агрегаты хранятся в разных местах. Если все находится в «одной базе данных», вам это может сойти с рук. Но как только вы вводите вторую базу данных, вы фактически вводите «распределенную транзакцию», с которой гораздо труднее иметь дело.

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

What happens in case we need to replay some events to bring only a certain aggregate's view in a consistent state?

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

Обычно это работает так: мы можем использовать идентификатор корреляции (например, идентификатор агрегата) для фильтрации потока событий. Таким образом, данная модель чтения не должна воспроизводить события все, а только подмножество (я) событий.

Is it right to replay partially some events or all events in the event store should be replayed in order to bring the whole system into a consistent state?

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

Возможно, вам будет полезно просмотреть этот Выступление Грега Янга, 2014 г.

1) Думаю, ваша модель не имитирует предметную область. Например: Вы назначаете команду («AddAgentToDepartment») на основе соглашения CRUD, а не процесса бизнес-домена, который в этом случае может либо назначать агента отделу, либо выделять отдел агенту.

2) Кто в этой ситуации является контролером / менеджером / привратником? Обязан ли отдел обеспечить выполнение всех бизнес-правил при назначении агента? Или это ответственность агентов за выбор подразделения и обеспечение его соответствия установленным бизнес-правилам?

3) Я бы посоветовал заново подумать о поднятии двух разных событий? Вероятно, вполне нормально вызвать одно событие и создать проекцию, которая отслеживает отношения агент <-> отдел.

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

Другие вопросы по теме