Рубрики
Без рубрики

Упрощение приложения с источником событий

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

Каждый раз, когда вы вносите изменение в состояние приложения, вы записываете это изменение как событие. Вы можете воспроизвести события с начала записи до определенного времени. Затем вы воссоздали состояние приложения в то время.

Вот что такое 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, List 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)
        );
    }

В этом коде используется 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, List events) {
    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”