Источник событий: как преобразовать один агрегат в другой

Собственно вопрос:

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

  • преобразовать агрегат в другой,

  • сохранить тот же идентификатор,

  • и по-прежнему сможете восстановить его из потока событий?

Теперь мой пример:

у меня есть ProspectiveCustomer, который можно преобразовать в PayingCustomer вот так:

ProspectiveCustomer::convertToPayingCustomer(ProspectiveCustomerId $id)

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

Итак, теперь представьте себе следующий поток событий:

  1. ProspectiveCustomer был добавлен в CRM
  2. ProspectiveCustomer получил предложение
  3. ProspectiveCustomer принял предложение и поэтому был преобразован в PayingCustomer
  4. PayingCustomer оплатил счет

Давайте сосредоточимся на пункте 4):

У нас будет commandHandler, который получает paymentCommand {customerId: 123, amount: 500 €}. Его работа будет заключаться в следующем:

  1. восстановить PayingCustomer на основе его истории событий
  2. call PayingCustomer :: pay (Деньги, сумма в долларах)

Мой вопрос касается 1) восстановления из истории:

Служба EventStorage:

  1. найдите AggregateId
  2. загружает события (SELECT * FROM Events WHERE ID = 'xxx')

Стек событий теперь будет содержать:

  • ПерспективныйКлиентWasAdded
  • Перспективный заказчик
  • ПерспективныйКлиентПринятоПредложение

Как может commandHandler обрабатывать PayingCustomer::reconstituteFromHistory(EventsHistory $events), в то время как $ events являются событиями, выданными из / применимыми к ProspectiveCustomer

ИЗМЕНИТЬ

в настоящее время я решаю проблему с PayingCustomer, имеющим собственный идентификатор, но имеющим ссылку на ProspectiveCustomerId.

Но учитывая то, что:

  1. это тот же ограниченный контекст,
  2. тот самый жизненный цикл клиента (ProspectiveCustomer заканчивается, когда начинается PayingCustomer),

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

Если бы это была не система, основанная на событиях, я бы определенно выбрал один уникальный идентификатор.

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


person LauDem    schedule 03.03.2016    source источник
comment
конечно, у вас будет ProspectiveCustomerConvertedToPayingCustomer Событие. И применить его к PropsectiveCustomer, вернув PayingCustomer   -  person LauDem    schedule 03.03.2016
comment
но проблема заключается в восстановлении из потока событий после того, как он был преобразован   -  person LauDem    schedule 03.03.2016
comment
Я думаю, что «платежеспособный клиент» и «потенциальный клиент» «констатируют», а не сущность. Вот где я ошибаюсь. извините за путаницу.   -  person Ryan Vincent    schedule 03.03.2016
comment
Спасибо за вашу заботу. У потенциального клиента могут быть совершенно разные инварианты, чем у PayingCustomer, поэтому необходимо разбить большой CustomerAggreagte на 2 меньших, но более точных AR.   -  person LauDem    schedule 03.03.2016
comment
Вы не конвертируете, вы создаете новый агрегат. Когда вы создаете агрегат, вы в любом случае используете внешний идентификатор, который предоставляется вам в событии. Вы копируете всю совокупную информацию о потенциальных клиентах, которая должна быть у вас в совокупности платящих клиентов. В двух словах - никакой совокупной конверсии, только нормальный бизнес-поток.   -  person Alexey Zimarev    schedule 04.03.2016
comment
›› без совокупной конверсии, только нормальный бизнес-поток. - ›ДА ДА знаю! я написал ProspectiveCustomer::convertToPayingCustomer(ProspectiveCustomerId $id), который генерирует событие PropsectiveCustomerConvertedToPayingCustomer ($ customerId)   -  person LauDem    schedule 04.03.2016


Ответы (2)


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

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

  1. Предполагаемый клиент создан
  2. Предложение принято
  3. Платежный клиент, созданный из потенциального клиента
  4. Потенциальный клиент удален (или помечен как "преобразованный", или деактивированный)

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

Вам нужны обработчики событий домена, которые выполняли бы (3) и (4), поскольку вы говорите, что это тот же ограниченный контекст.

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

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

person Alexey Zimarev    schedule 04.03.2016
comment
›› у вас появляются предположения, что вы всегда ожидаете, что у вас будет потенциальный клиент, чтобы получить свою историю или что-то еще, ||| нет я не - person LauDem; 04.03.2016
comment
›› Вы можете легко сохранить ссылку на идентификатор потенциального покупателя в новом платежеспособном покупателе || это то, что я сейчас делаю, как указано в моем Edit. Но это кажется беспорядочным (см. Править). Опять же, если бы у меня не было проблемы с моим стеком событий, содержащим события двух разных агрегатов, я бы сохранил Id. Это возвращает нас к моим 3 последним комментариям к приведенному выше ответу. - person LauDem; 04.03.2016
comment
Как я уже упоминал в своем ответе, я настоятельно рекомендую использовать идентификационную ссылку на ProspectiveCustomer из PayingCustomer без каких-либо предположений. - person Alexey Zimarev; 04.03.2016
comment
Это то, что я сейчас делаю (см. ИЗМЕНИТЬ), но только потому, что я использую поиск событий. Если бы у меня была постоянная система с обычным состоянием, я бы сохранил тот же идентификатор. Почему мое хранилище должно определять мое моделирование? - person LauDem; 04.03.2016
comment
Ну я уже ответил. Я не уверен, что вы настаиваете на своей идее повторного использования идентификатора. Вы пытаетесь сделать то, что в настоящее время является явным, неявным. И это не улучшение. Я не упомянул источник событий в своем ответе, я бы сделал это в любом случае, независимо от того, как я сохраняю свои агрегаты. - person Alexey Zimarev; 04.03.2016
comment
когда я говорю, сохраняйте тот же идентификатор, я имею в виду что-то вроде new PayingCustomer(new PayingCustomerId($prospectiveCustomerId->toString()) - person LauDem; 04.03.2016
comment
Вы пытаетесь сделать то, что в настоящее время является явным, неявным. ›› именно здесь я действительно запутался. Почему более явно иметь [разные идентификаторы + удерживающая ссылка], чем один идентификатор, учитывая его один жизненный цикл в одном и том же ограниченном контексте .... - person LauDem; 04.03.2016

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

Когда вы конвертируете свой агрегат, вы действительно создаете новый агрегат, поэтому я бы решил эту проблему с помощью обработчика событий домена. Обработчик событий домена будет реагировать на события и выдавать команды, поэтому пусть ваш ProspectiveCustomer отправит что-то вроде OfferAcceptedEvent, на которое обработчик событий может воздействовать.

Это может быть поток процесса:

  1. Пользователь принимает предложение и ProspectiveCustomer отправляет OfferAcceptedEvent.
  2. Обработчик событий реагирует на OfferAcceptedEvent и отправляет CreatePayingCustomerCommand. (OfferAcceptedEvent должен содержать все данные из ProspectiveCustomer, необходимые для создания команды)
  3. PayingCustomer создан

Вероятно, неплохо было бы включить ProspectiveCustomerId в PayingCustomerCreatedEvent, чтобы вы могли отслеживать PayingCustomer обратно до ProspectiveCustomer.

person user707727    schedule 04.03.2016
comment
Короче говоря, у вас не будет того же идентификатора, но у PayingCustomer есть ссылка на ProspectiveCustomerId. Я правильно понимаю? - person LauDem; 04.03.2016
comment
Да, исходный идентификатор принадлежит ProspectiveCustomer. У PayingCustomer должен быть собственный идентификатор. - person user707727; 04.03.2016
comment
это то, чем я сейчас занимаюсь, но это кажется беспорядочным. Учитывая, что это тот же ограниченный контекст, и это тот же перон, кажется, что он должен сохранять один и тот же идентификатор, потому что у каждого из них, безусловно, есть свой собственный жизненный цикл, но один заканчивается, когда начинается другой. - person LauDem; 04.03.2016
comment
Saga координирует последовательные действия, а менеджеры процессов выполняют длительные процессы. Вам не нужна сага, чтобы подписаться на одно доменное событие и отдать команду. Подойдет обычный обработчик событий домена. - person Alexey Zimarev; 04.03.2016
comment
@Developpeurtunisie, в чем именно проблема с сохранением идентификатора? Ваш агрегатный конструктор получает идентификатор в качестве параметра конструктора. Если нет, вам следует подумать о его изменении. Выдача и назначение идентификаторов агрегатов не должны выполняться внутри самих агрегатов. - person Alexey Zimarev; 04.03.2016
comment
Моя проблема с сохранением идентификатора - это воссоздание агрегата из стека событий, как описано в абзаце давайте сосредоточимся на пункте 4). - person LauDem; 04.03.2016
comment
Короче говоря, когда дело доходит до обработки paymentCommand {$ customerId, $ amount}, обработчику потребуется воссоздать PayingCustomer. Для этого он загрузит стек событий для Id (с запросом к БД, таким как SELECT *, где ID = 'xxx'). И если сохранить тот же идентификатор, стек событий будет содержать события от ProspectiveCustomer. - person LauDem; 04.03.2016
comment
как упоминалось в моем EDIT, я знаю, что что-то не так в моем понимании, потому что, если бы это не была система ES, я бы определенно сохранил тот же идентификатор. И поскольку ES должен быть только деталью реализации, это, вероятно, означает, что в моей реализации хранилища событий чего-то не хватает. - person LauDem; 04.03.2016
comment
@Developpeurtunisie старается избегать повторного использования одного и того же идентификатора для разных агрегатов. Если это разные объекты в вашем домене, они должны иметь разные ID: s. - person user707727; 05.03.2016
comment
@AlexeyZimarev В чем разница между сагой и обработчиком событий домена, который выдает команды в системе CQRS / ES? Кроме того, что сага может удерживать состояние (при необходимости), я не вижу разницы. - person user707727; 05.03.2016
comment
Здесь есть хороший обзор рабочего процесса, конечного автомата, саги и диспетчера процессов kellabyte.com/2012/05/30/clarifying-the-saga-pattern. Сага - другое дело, она выполняет команды в определенном порядке и обеспечивает компенсирующие действия для отката всей последовательности, если это необходимо. Обработчик событий домена ничего подобного не делает, я не уверен, зачем применять сложный шаблон к простой вещи. - person Alexey Zimarev; 05.03.2016
comment
@AlexeyZimarev - Учитывая, что сага предназначена для обработки неудачных действий, сага не подходит для решения этой проблемы. Я соответственно изменил свой ответ. Спасибо. - person user707727; 08.03.2016