Каждый раз, когда вы вносите изменения в состояние приложения, вы записываете это изменение как событие.
Вы можете воспроизводить события с начала записи до определенного времени. Затем вы воссоздали состояние приложения на тот момент.
Вот в чем суть Event Sourcing. Это похоже на то, что вы можете отправиться в прошлое во времени. Я нахожу это увлекательным.
Источники событий обеспечивают контрольный журнал, когда вам необходимо выполнить нормативные требования. Это может помочь с отладкой. И вы даже можете исследовать альтернативные реальности: что бы произошло, если бы…
Недавно я увидел отличный доклад Якуба Пилимона и Кенни Бастани о поиске событий.
Доклад представляет собой 1-часовой сеанс кодирования жизни. Два докладчика начинают с простого приложения, которое не связано с событиями. Затем они реорганизуют его, чтобы использовать события.
В конечном итоге они подключают приложение к Apache Kafka. Я пропущу эту часть в этой статье и сосредоточусь на концептуальной части поиска событий.
Резюме разговора
Как пользователь приложения для управления кредитными картами вы можете:
- Назначьте лимит кредитной карте
- Снять деньги со счета
- Вернуть деньги
Для каждой из этих команд есть метод в классе CreditCard
.
Вот исходный код метода assignLimit
:
public void assignLimit(BigDecimal amount) {
if(limitAlreadyAssigned()) {
throw new IllegalStateException();
}
this.initialLimit = amount;
}
Вот метод withdraw
:
public void withdraw(BigDecimal amount) {
if(notEnoughMoneyToWithdraw(amount)) {
throw new IllegalStateException();
}
if(tooManyWithdrawalsInCycle()) {
throw new IllegalStateException();
}
this.usedLimit = usedLimit.add(amount);
withdrawals++;
}
Метод repay
похож.
Помните, что для поиска событий вам необходимо записывать событие
каждый раз, когда приложение меняет свое состояние?
Таким образом, динамики извлекают каждое изменение состояния в свой собственный метод в классе 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; }
Экземпляр CardWithdrawn
представляет событие, когда пользователь успешно снял деньги. После изменения состояния событие добавляется в список ожидающих событий.
Вы вызываете метод save
класса CreditCardRepository, чтобы сбросить ожидающие события в поток событий. Затем слушатели событий могут обрабатывать события.
Помимо полезной нагрузки, каждое событие имеет свой уникальный идентификатор и временную метку. Таким образом, вы можете упорядочить и воспроизвести события позже.
Чтобы воспроизвести события для конкретной кредитной карты, репозиторий вызывает метод recreateFrom
класса CreditCard, передавая идентификатор карты и события, сохраненные для нее. :
public static CreditCard recreateFrom(UUID uuid, List<DomainEvent> events) { 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) ); }
Этот код использует библиотеку vavr.io для вызова метода handle
для каждого события. Метод handle
отправляет объект события соответствующему методу.
Например: для каждого события LimitAssigned
метод handle
вызывает метод limitAssigned
с событием в качестве параметра.
Упрощение приложения
Для упрощения кода я использовал библиотеку Требования как код. Сначала я помещаю в модель все классы событий и методы обработки. Нравится:
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();
Мне пришлось изменить возвращаемый тип методов обработки (например, limitAssigned
) на void
. Кроме того, преобразование из vavr.io было прямым.
Затем я создал бегунок и запустил его для модели:
this.modelRunner = new ModelRunner();
modelRunner.run(eventHandlingModel);
После этого я изменил методы recreateFrom
и handle
на следующие:
public static CreditCard recreateFrom(UUID uuid, List<DomainEvent> events) { CreditCard creditCard = new CreditCard(uuid); events.forEach(ev -> creditCard.handle(ev)); return creditCard; }
private void handle(DomainEvent event) { modelRunner.reactTo(event); }
На этом этапе я мог избавиться от зависимости от vavr.io.
Переход завершен. Теперь я мог бы сделать еще несколько упрощений.
Я снова обратился к методу withdraw
:
public void withdraw(BigDecimal amount) {
if(notEnoughMoneyToWithdraw(amount)) {
throw new IllegalStateException();
}
if(tooManyWithdrawalsInCycle()) {
throw new IllegalStateException();
}
cardWithdrawn(new CardWithdrawn(uuid, amount, Instant.now()));
}
Проверка tooManyWithdrawalsInCycle()
не зависела от данных события. Это зависело только от состояния CreditCard
.
Подобные проверки состояния могут быть представлены в модели как when
условия.
После того, как я перенес все проверки состояния для всех методов в модель, это выглядело так:
this.eventHandlingModel =
Model.builder()
.when(this::limitNotAssigned)
.on(LimitAssigned.class).system(this::limitAssigned)
.when(this::limitAlreadyAssigned)
.on(LimitAssigned.class).system(this::throwsException)
.when(this::notTooManyWithdrawalsInCycle)
.on(CardWithdrawn.class).system(this::cardWithdrawn)
.when(this::tooManyWithdrawalsInCycle)
.on(CardWithdrawn.class).system(this::throwsException)
.on(CardRepaid.class).system(this::cardRepaid)
.on(CycleClosed.class).system(this::cycleWasClosed)
.build();
Чтобы это сработало, мне нужно было заменить прямые вызовы методов, изменяющих состояние, методом handle
. После этого методы assignLimit
и withdraw
выглядели так:
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); }
Как видите, большая часть условной логики переехала из методов в модель. Это упрощает понимание методов.
Меня беспокоило то, что вы не должны забывать добавлять событие к ожидающим событиям. Каждый раз. Или ваш код не будет работать.
Требования как код позволяет контролировать, как система обрабатывает события. Итак, я извлек pendingEvents.add(event)
из методов:
modelRunner.handleWith(this::addingPendingEvents); ...
public void addingPendingEvents(StandardEventHandler eventHandler) { eventHandler.handleEvent(); DomainEvent domainEvent = (DomainEvent)eventHandler.getEvent(); pendingEvents.add(domainEvent); }
Я мог бы пойти дальше и извлечь логику проверки.
Но я оставляю это тебе в качестве упражнения на размышления, дорогой читатель.
В чем смысл?
Я пытался добиться четкого разделения проблем:
- Выполнение методов в зависимости от состояния определяется в модели.
- Проверка данных и изменения состояния находятся в реализациях методов
- События автоматически добавляются к ожидающим событиям. В целом: код инфраструктуры четко отделен от бизнес-логики.
Упрощение и без того очень простого примера полезно для объяснения.
Но я хочу не об этом говорить.
Дело в том, что на практике такое четкое разделение проблем окупается.
Особенно, если вы работаете с несколькими командами. О сложных проблемах.
Разделение проблем помогает изменять разные части кода с разной скоростью. У вас есть простые правила, где что-то искать. Код легче понять. И проще изолировать блоки для целей тестирования.
Заключение
Надеюсь, вам понравилась моя статья. Пожалуйста, оставляйте отзывы в комментариях.
Вы работали над приложениями для поиска событий?
Каковы были ваши впечатления?
Можете ли вы относиться к тому, что я написал в этой статье?
Я также хочу предложить вам посмотреть мою библиотеку, которую я использовал на протяжении всей статьи. Я буду очень рад, если вы попробуете это на практике и расскажете, что думаете.