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

Недавно я начал свою первую попытку разработать веб-приложение для продажи билетов, используя принципы проектирования, ориентированные на предметную область, в сочетании с поиском событий и 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 или, по крайней мере, у вас есть опыт, я хотел бы получить несколько подсказок о том, что, по вашему мнению, я делаю или думаю неправильно и в каком направлении мне следует смотреть.


person Thanasis Ioannidis    schedule 26.10.2018    source источник


Ответы (3)


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

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

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

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

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

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

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

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

AddAgentToDepartment(agentId, departmentId)

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

AddAgentToDepartment(departmentId, params: { agentIdToAdd })

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

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

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

Мой первый вопрос: правильно ли сохранять идентификаторы связанных агрегатов в состоянии агрегата?

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

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

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

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

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

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

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

person Roman Eremin    schedule 26.10.2018

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

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

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

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

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

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

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

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

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

person VoiceOfUnreason    schedule 26.10.2018

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

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

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

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

person Nitin babariya    schedule 01.11.2018