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

10

Я пытаюсь изучить способы DDD и смежных предметов. Мне пришла в голову идея простого ограниченного контекста для реализации «банка»: есть счета, деньги можно вкладывать, снимать и переводить между ними. Также важно вести историю изменений.

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

При рассмотрении депозитов и выводов - это относительно просто, потому что изменен только один агрегат.

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

Мой вопрос - как я могу объединить эти три принципа: «одна совокупность - одна транзакция», «событие -> изменение в совокупности» и «предотвращение одновременных изменений»?

cocsackie
источник

Ответы:

7

При переводе это отличается - два агрегата должны быть изменены одним событием MoneyTransferred.

Перевод денег является отдельным актом от обновления бухгалтерских книг.

MoneyTransferred
AccountCredited
AccountDebited

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

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

Ответ, конечно, прямо на вездесущем языке - счета зачисляются или дебетуются, чтобы отразить обязательства банка перед своими клиентами.

Хорошо, но это означает, что я должен использовать события AccountCredited и AccountDebited для депозитов и снятий, поэтому я регистрирую не причину изменений, а изменение, вызванное другими действиями. Если бы я хотел отменить действие, я не смог бы, потому что не все события зарегистрированы.

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

Второе - это значит, что мне нужно использовать что-то вроде саги.

Немного другое написание: вам нужно что-то вроде человека, посылающего правильные команды .

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

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

VoiceOfUnreason
источник
Хорошо, но это означает, что я должен использовать события AccountCredited и AccountDebited для депозитов и снятий средств, поэтому я регистрирую только не причину изменения, а изменение, вызванное каким-либо другим действием. Если бы я хотел отменить действие, я не смог бы, потому что не все события зарегистрированы. Как я могу это сделать (причинность событий)? Второе - это значит, что мне нужно использовать что-то вроде саги. Как тогда должен быть смоделирован перевод? На данный момент у меня есть способ перевода на счет. При вызове публикует событие MoneyTransferred . Я не знаю, с чего начать что-то вроде саги.
cocsackie
Разве это не -> AccountCredited и AccoundDebited, а затем MoneyTransferred ? Первое решение обновляет оба агрегата в одной транзакции (без какой- либо гарантии согласованности )? Также нет агрегата, который мог бы опубликовать MoneyTransferred -> без корреляции. Второе решение кажется более подходящим - ProcessTransaction может публиковать MoneyTransferred и, чтобы избежать множественных совокупных изменений в одной транзакции, я могу публиковать события со счета после совершения транзакции. Извините за привередливость. Это трудно понять новичку - нельзя использовать только один шаблон без другого.
коксаки
1

Важная деталь в понимании учетных записей на основе транзакций: balanceатрибут accountфактически является примером денормализации. Это для удобства. На самом деле баланс счета - это сумма транзакций, и вам не нужен сам счет для баланса.

Принимая это во внимание, акт перевода денег должен состоять не в обновлении, accountа в вставке transaction.

При этом есть еще одно важное правило: акт добавления transactionдолжен быть атомарным с обновлением поля (денормализованного баланса) account.

Теперь, если я понимаю концепцию агрегатов DDD, мне кажется важным следующее :

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

Так что с точки зрения дизайна DDD я бы предложил:

  1. Существует один агрегат для представления передачи

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

  3. Любой доступ к передаче должен обеспечиваться корневым объектом ( transfer).

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

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

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

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

Джон Ву
источник
0

Я также в настоящее время на стадии обучения. С точки зрения реализации, вот как я чувствую, вы выполните это действие.

Отправьте TransferMoneyCommand, который вызывает следующие события [MoneyTransferEvent, AccountDebitedEvent]

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

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

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

AccountDebitedEvent удалит деньги со счета плательщика (обновит совокупное состояние и любые связанные модели представления / проекции)

MoneyTransferEvent запускает Saga / Process Manager.

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

Saga / Process manager опубликует команду CreditAccountCommand, которая применяется к учетной записи получателя, и в случае успеха поднимается значение AccountCreditedEvent.

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

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

Шаян С
источник