Каждый раз, когда вы вносите изменение в состояние приложения, вы записываете это изменение как событие. Вы можете воспроизвести события с начала записи до определенного времени. Затем вы воссоздали состояние приложения в то время.
Вот что такое 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”