Каждый раз, когда вы вносите изменение в состояние приложения, вы записываете это изменение как событие. Вы можете воспроизвести события с начала записи до определенного времени. Затем вы воссоздали состояние приложения в то время.
Вот что такое Event Sourcing . Это как будто ты можешь путешествовать во времени в прошлое. Я нахожу это захватывающим.
Источник событий предоставляет контрольный журнал, когда вам необходимо соответствовать нормативным требованиям. Это может помочь в отладке. И вы даже можете исследовать альтернативные реальности: что бы произошло, если бы…
Недавно я видел отличный доклад от Якуб Пилимон и Кенни Бастани о поиск источников событий.
Беседа представляет собой 1-часовую сессию кодирования жизни. Два докладчика начинают с простого приложения, которое не является источником событий. Затем они реорганизуют его, чтобы использовать события.
В конечном итоге они подключают приложение к Apache Kafka. Я пропущу эту часть в этой статье и вместо этого сосредоточусь на концептуальной части поиска событий.
Краткое изложение выступления
Как пользователь приложения для управления кредитными картами, вы можете:
- Назначьте лимит для кредитной карты
- Вывести деньги
- Вернуть деньги
Для каждой из этих команд существует метод в классе Credit Card
. Вот исходный код метода assign Limit
:
public void assignLimit(BigDecimal amount) { if(limitAlreadyAssigned()) { throw new IllegalStateException(); } this.initialLimit = amount; }
Вот метод вывода средств
:
public void withdraw(BigDecimal amount) { if(notEnoughMoneyToWithdraw(amount)) { throw new IllegalStateException(); } if(tooManyWithdrawalsInCycle()) { throw new IllegalStateException(); } this.usedLimit = usedLimit.add(amount); withdrawals++; }
Метод repair
аналогичен.
Помните, что для поиска событий вам нужно записывать событие каждый раз, когда приложение меняет свое состояние? Таким образом, динамики извлекают каждое изменение состояния в свой собственный метод в классе CreditCard .
Вот переработанный метод withdraw
:
public void withdraw(BigDecimal amount) { if(notEnoughMoneyToWithdraw(amount)) { throw new IllegalStateException(); } if(tooManyWithdrawalsInCycle()) { throw new IllegalStateException(); } cardWithdrawn(new CardWithdrawn(uuid, amount, Instant.now())); } private CreditCard cardWithdrawn(CardWithdrawn event) { this.usedLimit = usedLimit.add(event.getAmount()); withdrawals++; pendingEvents.add(event); return this; }
Экземпляр Карта снята
представляет событие, в котором пользователь успешно снял деньги. После того, как состояние изменилось, событие добавляется в список ожидающих событий.
Вы вызываете метод save
класса Credit Card Repository , чтобы сбросить ожидающие события в поток событий. Затем прослушиватели событий могут обрабатывать события.
Помимо полезной нагрузки, каждое событие имеет свой собственный уникальный идентификатор и временную метку. Таким образом, вы можете упорядочить и воспроизвести события позже. Чтобы воспроизвести события для конкретной кредитной карты, хранилище вызывает метод createform
класса CreditCard , передавая идентификатор карты и события, сохраненные для нее:
public static CreditCard recreateFrom(UUID uuid, Listevents) { return ofAll(events).foldLeft(new CreditCard(uuid), CreditCard::handle); } private CreditCard handle(DomainEvent event) { return Match(event).of( Case($(Predicates.instanceOf(LimitAssigned.class)), this::limitAssigned), Case($(Predicates.instanceOf(CardWithdrawn.class)), this::cardWithdrawn), Case($(Predicates.instanceOf(CardRepaid.class)), this::cardRepaid), Case($(Predicates.instanceOf(CycleClosed.class)), this::cycleWasClosed) ); }
В этом коде используется var.io библиотека для вызова метода handle |/для каждого события. Метод
handle отправляет объект события соответствующему методу.
Например: для каждого события Limit, назначенного , метод
handle вызывает метод
limit, назначенный , с событием в качестве параметра.
Упрощение применения
Я использовал requirements в качестве библиотеки code для упрощения кода. Во-первых, я поместил все классы событий и методы обработки в модель. Подобный этому:
this.eventHandlingModel = Model.builder() .on(LimitAssigned.class).system(this::limitAssigned) .on(CardWithdrawn.class).system(this::cardWithdrawn) .on(CardRepaid.class).system(this::cardRepaid) .on(CycleClosed.class).system(this::cycleWasClosed) .build();
Мне пришлось изменить возвращаемый тип методов обработки (например, limit Assigned
) на void
. Кроме того, преобразование из var.io был прямолинеен.
Затем я создал бегун и запустил его для модели:
this.modelRunner = new ModelRunner(); modelRunner.run(eventHandlingModel);
После этого я изменил воссоздать форму
и обработайте
методы для этого:
public static CreditCard recreateFrom(UUID uuid, Listevents) { CreditCard creditCard = new CreditCard(uuid); events.forEach(ev -> creditCard.handle(ev)); return creditCard; } private void handle(DomainEvent event) { modelRunner.reactTo(event); }
В этот момент я мог бы избавиться от зависимости от var.io . Переход завершен. Теперь я мог бы сделать еще несколько упрощений.
Я пересмотрел метод вывода средств
:
public void withdraw(BigDecimal amount) { if(notEnoughMoneyToWithdraw(amount)) { throw new IllegalStateException(); } if(tooManyWithdrawalsInCycle()) { throw new IllegalStateException(); } cardWithdrawn(new CardWithdrawn(uuid, amount, Instant.now())); }
Проверка слишком много изъятий В цикле()
не зависела от данных события. Это зависело только от состояния Кредитной карты
. Проверки состояния, подобные этой, могут быть представлены в модели как условия
.
После того, как я перенес все проверки состояния для всех методов в модель, это выглядело следующим образом:
this.eventHandlingModel = Model.builder() .condition(this::limitNotAssigned) .on(LimitAssigned.class).system(this::limitAssigned) .condition(this::limitAlreadyAssigned) .on(LimitAssigned.class).system(this::throwsException) .condition(this::notTooManyWithdrawalsInCycle) .on(CardWithdrawn.class).system(this::cardWithdrawn) .condition(this::tooManyWithdrawalsInCycle) .on(CardWithdrawn.class).system(this::throwsException) .on(CardRepaid.class).system(this::cardRepaid) .on(CycleClosed.class).system(this::cycleWasClosed) .build();
Чтобы это сработало, мне нужно было заменить прямые вызовы методов, которые изменяют состояние, методом handle
. После этого методы назначить лимит
и вывести
выглядели следующим образом:
public void assignLimit(BigDecimal amount) { handle(new LimitAssigned(uuid, amount, Instant.now())); } private void limitAssigned(LimitAssigned event) { this.initialLimit = event.getAmount(); pendingEvents.add(event); } public void withdraw(BigDecimal amount) { if(notEnoughMoneyToWithdraw(amount)) { throw new IllegalStateException(); } handle(new CardWithdrawn(uuid, amount, Instant.now())); } private void cardWithdrawn(CardWithdrawn event) { this.usedLimit = usedLimit.add(event.getAmount()); withdrawals++; pendingEvents.add(event); }
Как вы можете видеть, большая часть условной логики переместилась из методов в модель. Это облегчает понимание методов.
Одна вещь, которая меня беспокоила, – это то, что вы не должны забывать добавлять событие в список ожидающих событий. Каждый раз. Или ваш код не будет работать.
Требования в виде кода позволяет управлять тем, как система обрабатывает события. Поэтому я также извлек pending Events.add(event)
из методов:
modelRunner.handleWith(this::addingPendingEvents); ... public void addingPendingEvents(StepToBeRun stepToBeRun) { stepToBeRun.run(); DomainEvent domainEvent = (DomainEvent) stepToBeRun.getEvent().get(); pendingEvents.add(domainEvent); }
Я мог бы пойти дальше и также извлечь логику проверки. Но я оставляю это в качестве мыслительного упражнения для вас, дорогой читатель.
Какой в этом смысл?
Чего я пытался добиться, так это четкого разделения проблем:
- Зависящее от состояния выполнение методов определено в модели
- Проверка данных и изменения состояния находятся в реализациях методов
- События автоматически добавляются к ожидающим событиям. В целом: код инфраструктуры четко отделен от бизнес-логики.
Упрощение примера, который и так очень прост, полезно для объяснения. Но я хочу подчеркнуть не это.
Суть в том, что такое четкое разделение интересов окупается на практике. Особенно, если вы работаете с несколькими командами. По сложным проблемам.
Разделение проблем помогает изменять разные части кода в разном темпе. У вас есть простые правила, где что-то найти. Код легче понять. И проще изолировать модули для целей тестирования.
Вывод
Надеюсь, вам понравилась моя статья. Пожалуйста, дайте мне обратную связь в комментариях.
Работали ли вы над приложениями для поиска источников событий? На что был похож ваш опыт? Можете ли вы соотнести то, что я написал в этой статье?
Я также хочу пригласить вас взглянуть на мою библиотеку , которую я использовал на протяжении всей статьи. Я был бы в восторге, если бы вы попробовали это на практике и сказали мне, что вы думаете.
Отредактировано 21 ноября 2018 г.: исправлена одна ошибка копирования-вставки в примере кода
Оригинал: “https://dev.to/bertilmuth/simplifying-an-event-sourced-application-1klp”