Недавно я написал об упрощении приложения, основанного на событиях.

Статья начинается с кода из беседы Якуба Пилимона и Кенни Бастани. И это заканчивается построением модели событий в коде: как они применяются и при каких условиях.

Образец приложения посвящен управлению кредитной картой. Вы можете:

  • Назначьте кредитный лимит. Но только один раз, иначе приложение выдает IllegalStateException.
  • Вывод денег. Но вы не можете сделать больше 45 выводов за определенный цикл. Или вы тоже получите исключение.
  • Вернуть деньги

Я поигрался с CreditCardclass. У меня было ощущение, что с методом withdraw что-то не так. Итак, я написал тест, который проверяет правильность поведения.

@Test(expected = IllegalStateException.class)
public void withdrawWithoutLimitAssigndThrowsIllegalStateException() 
{
    CreditCard card = new CreditCard(UUID.randomUUID());
    card.withdraw(BigDecimal.ZERO);
}

Тест пытается снять нулевую сумму. Но до сих пор кредитный лимит не назначался. Приложение должно отклонить это и выдать IllegalStateException.
Вместо этого приложение закинуло NullPointerException.

Приложение предполагало, что ограничение было назначено ранее.
Теперь: это образец приложения. Если бы он охватил все случаи, это, вероятно, было бы не так понятно, как есть.

Представим, что мы имеем дело с реальным приложением. Что, если требуемый порядок команд / событий зависит от множества условий и состояний?

Если вы когда-либо пытались реализовать это с помощью только условных операторов, вы, вероятно, знаете, что легко потерять обзор. Но есть стандартное решение для управления сложными потоками и изменениями в поведении.

Государственная машина спешит на помощь

В информатике конечные автоматы существуют уже несколько десятилетий. Теоретически они хорошо понятны. Они зарекомендовали себя на практике. Они де-факто являются стандартом для решения проблем, связанных с поведением, зависящим от государства.

Поэтому я решил создать модель конечного автомата UML для примера приложения. Сначала я спросил себя: хочу ли я иметь дело с командами или событиями в конечном автомате?

Команды - это то, что приложение должно делать в будущем.
События связаны с тем, что произошло в прошлом.

Я хотел предотвратить снятие средств без установленного лимита кредита.
Итак, модель конечного автомата должна была иметь дело с командами.

Синтаксис перехода на схеме command[condition] / commandHandler(). Это означает: когда объект команды получен и условие выполнено, если оно присутствует, обработать команду и перейти к следующему состоянию.

Модель фиксирует, что разрешено, а что нет. Например: возврат возможен только после вывода.

Но у этой точности есть цена. Если вы хотите, чтобы модель конечного автомата выполнялась и управляла поведением во время выполнения, вам необходимо моделировать все возможные переходы из каждого состояния. Включая его состояние, если есть два перехода с одним и тем же событием.

Вот почему в конечном автомате намного больше повторений, чем в исходном коде с операторами if. Способ уменьшить количество повторений - использовать суперсостояния и подсостояния:

В модели конечного автомата легко определить поведение, зависящее от состояния. Но независимое от состояния правило, такое как в любом состоянии (когда выполняется условие X), do Y приводит к нескольким переходам. Например, мне нужно было добавить requestToCloseCycle в каждое суперсостояние.

Для создания моделей вам нужны люди с нужными навыками. А обсуждать модели с нетехническими заинтересованными сторонами непросто. О пути пользователя обычно говорят не так.

Прощаться

Кажется, пока есть два варианта.

В левом углу: инструкция if. Начать легко. Низкие накладные расходы. Лучше всего подходит для приложений, в которых нет сложных потоков поведения. Но легко потерять обзор, когда поведение усложняется.

В правом углу: исполняемая модель конечного автомата. Мощный. Проверено. Точный. Дает вам обзор поведения. Но трудно определить независимые от государства правила. А о моделях конечного автомата трудно сообщить заинтересованным сторонам нетехнического профиля.

Я стою в третьем углу. Я нашел альтернативу конечным автоматам.
Решение, которое

  • позволяет определять условия. Но в большинстве случаев это не обязательно.
  • делает одинаково легким определение зависимых от состояния и независимых правил.
  • использует язык, понятный всем заинтересованным сторонам.

Прежде чем я углублюсь в детали, вот образец модели конечного автомата, переписанный с использованием этого решения:

Как видите, модель в коде. Модель-бегун выполняет эту модель. Бегун реагирует на запросы / события аналогично конечному автомату.

Основной поток - это «сценарий счастливого дня». Шаги пользователя по достижению своей цели. Остальные потоки охватывают альтернативные сценарии и сценарии ошибок.

Поток может определять явное условие для своего первого шага, например, after(...), anytime() или when() в примере.
Если у потока есть явное условие, поток запускается, когда условие выполнено, и бегун в настоящее время находится в другом потоке.
Если у потока нет явного условия (например, основной поток в образце), первый шаг выполняется после запуска бегуна, когда до сих пор не было выполнено ни одного шага.

Начиная со второго шага потока, каждый шаг имеет неявное условие. Это условие: выполнить шаг после предыдущего в том же потоке, если не может быть запущен другой поток с явным условием.
Таким образом, в отличие от конечных автоматов, вам не нужно указывать условия после первого шага.

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

Взгляните на дальнейшие примеры, чтобы копнуть глубже.

Когда использовать требования в качестве кода

Многие приложения имеют динамическое внутреннее поведение. В частности, это верно для распределенных приложений. Им нужно иметь дело с тем, что «другая сторона» недоступна.

Но с точки зрения пользователя эти приложения выглядят вполне предсказуемо и регулярно. Когда я хочу посмотреть шоу на Netflix или Amazon Prime, я каждый раз выполняю одни и те же шаги, пока не смогу его посмотреть. Похоже, что один шаг следует за другим.

Это идеальное место для требований как кода, если оно используется в качестве альтернативы конечному автомату: определение видимого поведения приложения.

Как сейчас работает приложение кредитной карты

  • Клиент отправляет команду CreditCardAggregateRoot
  • CreditCardAggregateRoot использует репозиторий событий для воспроизведения всех событий кредитной карты, чтобы восстановить ее.
  • CreditCardAggregateRoot использует указанную выше модель для передачи команды методу обработки команд.
  • Метод обработки команды создает событие и применяет его к экземпляру CreditCard.
  • Модель обработки событий экземпляра CreditCard отправляет событие методу изменения состояния.

Заключение

Надеюсь, вам понравилась моя статья. Скажите, пожалуйста, что вы думаете в комментариях.

Я также хочу предложить вам взглянуть на библиотеку, которую я использовал на протяжении всей статьи. Попробуйте это на практике и дайте мне знать результат.

Есть вопросы? Поговорите со мной в Gitter.

Если вы хотите быть в курсе того, что я делаю, или напишите мне, подпишитесь на меня в LinkedIn или Twitter. Чтобы узнать о гибкой разработке программного обеспечения, посетите мой онлайн-курс.

Последнее изменение: 27 апреля 2020 г.: обновлен процесс поиска событий