* Нажмите здесь, чтобы перейти к части I

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

Наш домен

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

В этом распределении мы видим, что, когда покупатель решает разместить заказ, корзина покупок взаимодействует с субдоменом Order. В этот момент задействованы субдомены Payment и Shipping. В простейшей форме, как только финансовый аспект платежа подтвержден эмитентом карты, заказ может быть продолжен, и товары будут отправлены покупателю.

Наша команда отвечает за обработку этого фиктивного субдомена Payment, и мы решили использовать Event Sourcing. Как показано в Части I, мы должны выделить эволюцию состояния нашей системы во времени.

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

На рисунке 3 выделена одна команда / объект / событие. После проведения анализа мы согласовали следующий список:

Entity: Платеж

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

Создание объекта

В нашем домене у нас есть объект под названием Payment, который инкапсулирует свойства и поведение, которые представляют его возможные взаимодействия. Мы можем увидеть его диаграмму классов на рисунке 4 и выделить некоторые аспекты:

  1. Используемая терминология соответствует повсеместному языку домена;
  2. Он содержит объект значения (PaymentId);
  3. Он содержит список событий (набор DomainEvents);
  4. У него есть метод, указывающий на произошедшее событие (record_that);
  5. У него есть метод, очищающий список событий (clear_events). Это будет обсуждаться позже, когда мы извлечем сущность.

Если мы вспомним анализ, у нас есть серия взаимодействий и связанных событий, которые должны быть сгенерированы в результате успешного выполнения. На рисунке 5 показано одно из них, событие Payment Created, которое должно быть сгенерировано при успешном взаимодействии create.

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

Итак, давайте посмотрим на них в действии. Сначала мы начнем с создания события домена.

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

Обратите внимание, что создание происходит следующим образом:

  • Выполняется публичное поведение (создать)
  • Экземпляр создается только с идентификатором
  • Вы уведомляете свою вновь созданную сущность о том, что она должна записать, что произошло событие.
  • Все дополнительные свойства объекта обновляются.
  • Событие добавлено в список событий.

Обработка бизнес-инвариантов

Пока что нам удалось создать объект Payment. Но в реальных сценариях с большинством вариантов поведения связаны бизнес-инварианты. Например, при создании платежа сумма к оплате должна быть больше 0. В некоторых случаях это должно обрабатываться объектом значения, но для краткости давайте сделаем это в самой сущности.

Перед выполнением метода record_that необходимо выполнить всю бизнес-проверку.

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

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

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

В Python один из способов добиться этого можно увидеть в следующем фрагменте.

Сохранение сущности

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

В случае с хранилищем событий, хотя мы могли и дальше это делать, я решил не делать этого по следующим причинам:

  1. Хранилище событий - это второстепенная задача: я считаю, что мы должны максимально сосредоточиться на ядре приложения;
  2. Эффективное хранилище событий может быть сложно спроектировать / разработать: управление жизненным циклом потоков, создание изменчивых или постоянных подписок и объединение потоков - это функции, которые вам, вероятно, понадобятся или понадобятся для производственных систем. Насколько это возможно, мы не должны пытаться изобретать велосипед.

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

По сути, хранилище событий должно управлять созданием нескольких потоков событий, добавляя элементы к этим потокам и извлекая их эффективным способом. У вас может быть много вариантов реализации вашей: от использования обычных СУБД, таких как MySQL или PostgreSQL, до решений NoSQL с использованием MongoDB или DynamoDB. Хотя это жаркие споры, есть и те, кто даже ратует за использование для этой цели распределенных систем логов, таких как Kafka. Как и в любой другой ситуации, прежде чем выбирать решение, оцените свои требования и рассмотрите уровень знаний в ваших командах с точки зрения имеющихся вариантов.

Стримы в EventStore

Поток в EventStore идентифицируется уникальным именем, которое в нашем случае будет состоять из конкатенации имени объекта во множественном числе и его уникального идентификатора. Так, например, если мы сохраняем сущность Payment с идентификатором 1, имя потока будет payments-1.

Внутри любого потока у вас будут элементы, соответствующие записанным вами событиям. В EventStore это означает:

  1. Порядок событий: монотонно возрастающее число, начиная с 0;
  2. Внутреннее имя: создается EventStore путем объединения порядка событий и имени потока;
  3. Тип события: строка, указанная вами при сохранении события;
  4. Дата: дата, когда хранилище событий получило событие;
  5. Полезные данные JSON: содержащие фактические данные из события домена, предоставленные вами при сохранении события.

Событие PaymentCreated будет выглядеть так, как показано на рисунке 6.

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

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

Теперь, когда мы сохранили сущность Payment, мы сосредоточимся на том, как ее получить и восстановить состояние.

Поиск сущности

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

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

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

  1. Повторно создать объект только с его идентификатором;
  2. Получать события из потока с самого начала;
  3. Для каждого применяется к юридическому лицу.

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

Об исходном коде

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

Так что же дальше?

В этой статье я связал определение и взаимосвязь между основными движущимися частями Event Sourcing с конкретной реализацией. Это не только позволяет вам запускать собственное приложение, используя язык по вашему выбору, но также открывает более глубокое обсуждение более сложных сценариев, о которых вы должны знать, таких как создание прогнозов, способы обращения с очень длинными потоками событий, миграция событий и т. Д. и GDPR. Эти темы будут рассмотрены в части III этой серии статей, так что следите за обновлениями!

Редакционные обзоры от Deanna Chow, Liela Touré и Prateek Sanyal.

Хотите работать с нами? Нажмите здесь, чтобы увидеть все открытые вакансии на SSENSE!