* Нажмите здесь, чтобы перейти к части I
В первой части этой серии SSENSE-TECH я представил определение источника событий, его основных компонентов и жизненного цикла общих операций, выполняемых приложением, которое его использует. Во второй части я проведу вас через образец реализации, чтобы предоставить вам отправную точку, это послужит фоном для дальнейших обсуждений практических аспектов любого приложения, использующего Event Sourcing.
Наш домен
Представьте, что вы - компания онлайн-торговли, и вы организовали операции и системы в соответствии с тем, что показано на рисунке 1.
В этом распределении мы видим, что, когда покупатель решает разместить заказ, корзина покупок взаимодействует с субдоменом Order. В этот момент задействованы субдомены Payment и Shipping. В простейшей форме, как только финансовый аспект платежа подтвержден эмитентом карты, заказ может быть продолжен, и товары будут отправлены покупателю.
Наша команда отвечает за обработку этого фиктивного субдомена Payment, и мы решили использовать Event Sourcing. Как показано в Части I, мы должны выделить эволюцию состояния нашей системы во времени.
На рисунке 2 мы видим, что наш платеж сначала создается в состоянии ожидающий и может со временем развиваться по мере того, как мы с ним взаимодействуем. Эта диаграмма иллюстрирует события, которые будут иметь место, и ясно показывает взаимодействия (команды), которые нам придется обслуживать.
На рисунке 3 выделена одна команда / объект / событие. После проведения анализа мы согласовали следующий список:
Entity: Платеж
Каждый переход должен содержать бизнес-инварианты, чтобы контролировать, разрешен он или нет. Например, могут быть отменены только ожидающие или авторизованные платежи, или могут быть возвращены только урегулированные платежи.
Создание объекта
В нашем домене у нас есть объект под названием Payment, который инкапсулирует свойства и поведение, которые представляют его возможные взаимодействия. Мы можем увидеть его диаграмму классов на рисунке 4 и выделить некоторые аспекты:
- Используемая терминология соответствует повсеместному языку домена;
- Он содержит объект значения (PaymentId);
- Он содержит список событий (набор DomainEvents);
- У него есть метод, указывающий на произошедшее событие (record_that);
- У него есть метод, очищающий список событий (clear_events). Это будет обсуждаться позже, когда мы извлечем сущность.
Если мы вспомним анализ, у нас есть серия взаимодействий и связанных событий, которые должны быть сгенерированы в результате успешного выполнения. На рисунке 5 показано одно из них, событие Payment Created, которое должно быть сгенерировано при успешном взаимодействии create.
При разработке своих событий вы заметите, что все они имеют что-то общее: уникальный идентификатор (aggregate_id), связанный с сущностью, которой он принадлежит, и когда это произошло (Произошло_ат ). Каждое событие содержит только необходимую информацию, которая представляет, что изменилось, в данном случае amount_due.
Итак, давайте посмотрим на них в действии. Сначала мы начнем с создания события домена.
Рекомендуется, чтобы ваши события были неизменными и содержали только примитивные типы. Это поможет в процессе сериализации / десериализации, который происходит, когда вы сохраняете их в хранилище событий. Теперь, когда событие определено, давайте создадим нашу сущность.
Обратите внимание, что создание происходит следующим образом:
- Выполняется публичное поведение (создать)
- Экземпляр создается только с идентификатором
- Вы уведомляете свою вновь созданную сущность о том, что она должна записать, что произошло событие.
- Все дополнительные свойства объекта обновляются.
- Событие добавлено в список событий.
Обработка бизнес-инвариантов
Пока что нам удалось создать объект Payment. Но в реальных сценариях с большинством вариантов поведения связаны бизнес-инварианты. Например, при создании платежа сумма к оплате должна быть больше 0. В некоторых случаях это должно обрабатываться объектом значения, но для краткости давайте сделаем это в самой сущности.
Перед выполнением метода record_that необходимо выполнить всю бизнес-проверку.
Теперь давайте займемся еще одним взаимодействием. На этот раз мы реализуем возврат, и для этого правила следующие: возвращаемая сумма должна быть больше нуля, но не больше суммы, причитающейся к оплате, и платеж должен быть в урегулированном состоянии.
Также необходимо определить PaymentRefunded. Обратите внимание на следующий фрагмент, что мы снова просто добавили информацию о том, что изменилось.
Вы можете следовать аналогичному пути для других взаимодействий с вашей сущностью. Важно отметить, что реализация record_that, представленная до сих пор, очень наивна, и на практике большинство языков допускают более динамические способы привязки определенного метода к вызову на основе типа события.
В Python один из способов добиться этого можно увидеть в следующем фрагменте.
Сохранение сущности
Для сохранения объекта необходимо сохранить список новых событий в хранилище событий объекта, которым управляют. Пока что мы смогли предоставить реализацию, которая не требует внешних зависимостей, включая использование какой-либо структуры. Это было сделано не случайно, но чтобы попытаться сделать ваш домен максимально независимым от среды.
В случае с хранилищем событий, хотя мы могли и дальше это делать, я решил не делать этого по следующим причинам:
- Хранилище событий - это второстепенная задача: я считаю, что мы должны максимально сосредоточиться на ядре приложения;
- Эффективное хранилище событий может быть сложно спроектировать / разработать: управление жизненным циклом потоков, создание изменчивых или постоянных подписок и объединение потоков - это функции, которые вам, вероятно, понадобятся или понадобятся для производственных систем. Насколько это возможно, мы не должны пытаться изобретать велосипед.
По этим причинам я выбрал стандартный магазин событий под названием EventStore. Хотя код в этой статье ни в коем случае не исчерпывающе использует свои функции, я обнаружил, что он предлагает хороший набор функций прямо из коробки и действительно легко интегрируется.
По сути, хранилище событий должно управлять созданием нескольких потоков событий, добавляя элементы к этим потокам и извлекая их эффективным способом. У вас может быть много вариантов реализации вашей: от использования обычных СУБД, таких как MySQL или PostgreSQL, до решений NoSQL с использованием MongoDB или DynamoDB. Хотя это жаркие споры, есть и те, кто даже ратует за использование для этой цели распределенных систем логов, таких как Kafka. Как и в любой другой ситуации, прежде чем выбирать решение, оцените свои требования и рассмотрите уровень знаний в ваших командах с точки зрения имеющихся вариантов.
Стримы в EventStore
Поток в EventStore идентифицируется уникальным именем, которое в нашем случае будет состоять из конкатенации имени объекта во множественном числе и его уникального идентификатора. Так, например, если мы сохраняем сущность Payment с идентификатором 1, имя потока будет payments-1.
Внутри любого потока у вас будут элементы, соответствующие записанным вами событиям. В EventStore это означает:
- Порядок событий: монотонно возрастающее число, начиная с 0;
- Внутреннее имя: создается EventStore путем объединения порядка событий и имени потока;
- Тип события: строка, указанная вами при сохранении события;
- Дата: дата, когда хранилище событий получило событие;
- Полезные данные JSON: содержащие фактические данные из события домена, предоставленные вами при сохранении события.
Событие PaymentCreated будет выглядеть так, как показано на рисунке 6.
В нашем случае я использовал шаблон репозитория для получения Payment и клиента EventStore для сохранения событий.
На рисунке 7 вы можете видеть, что есть интерфейс только с двумя методами, которые нам понадобятся, и конкретная реализация, специфичная для EventStore.
Теперь, когда мы сохранили сущность Payment, мы сосредоточимся на том, как ее получить и восстановить состояние.
Поиск сущности
Предполагается, что наша организация Платеж со временем будет развиваться. Таким образом, помимо сохранения его при создании, второй вариант использования, который вам нужно будет удовлетворить, - это возможность получить существующий и выполнить в нем последующие изменения. Если вы обратитесь к рис. 2, наш Платеж создается в состоянии ожидания и при определенных условиях может переходить в другое состояние, например Возврат. Предположим, у нас есть один Платеж, который уже был сохранен в нашем хранилище событий, и мы хотим вернуть его.
Помните, что для восстановления состояния объекта вы начинаете с извлечения событий и их применения в том же порядке, в котором они были добавлены в поток.
Помимо синтаксического аспекта, который может варьироваться от языка к языку, последовательность действий такова:
- Повторно создать объект только с его идентификатором;
- Получать события из потока с самого начала;
- Для каждого применяется к юридическому лицу.
Поскольку мы применяем ранее сохраненные события, нет необходимости проверять какие-либо бизнес-правила.
Об исходном коде
Полную реализацию приложения вы можете найти здесь. Он содержит все сущности, события и репозитории для приложения. Обратите внимание, что хотя цель состоит в том, чтобы предоставить конкретную реализацию, она не считается готовой к производству, поскольку для краткости она не включает ключевые функции, такие как: оптимизация производительности, полная обработка ошибок, полное использование концепций DDD и тестирование покрытие.
Так что же дальше?
В этой статье я связал определение и взаимосвязь между основными движущимися частями Event Sourcing с конкретной реализацией. Это не только позволяет вам запускать собственное приложение, используя язык по вашему выбору, но также открывает более глубокое обсуждение более сложных сценариев, о которых вы должны знать, таких как создание прогнозов, способы обращения с очень длинными потоками событий, миграция событий и т. Д. и GDPR. Эти темы будут рассмотрены в части III этой серии статей, так что следите за обновлениями!
Редакционные обзоры от Deanna Chow, Liela Touré и Prateek Sanyal.
Хотите работать с нами? Нажмите здесь, чтобы увидеть все открытые вакансии на SSENSE!