Статьи

Akka Notes — Конечные автоматы — 1

Недавно у меня была возможность поиграть с Akka FSM на работе для действительно интересного варианта использования. API (на самом деле, DSL) довольно крутой, и весь опыт был потрясающим. Вот моя попытка записать мои заметки о создании конечного автомата с использованием Akka FSM. В качестве примера, мы пройдемся по шагам создания (ограниченного) автомата по продаже кофе.

Почему бы не become и не become ?

Мы знаем, что простые ванильные актеры Akka могут переключать свое поведение с помощью становления / unbecome. Тогда зачем нам Akka FSM? Разве простой актер не может просто переключаться между штатами и вести себя по-другому? Да, это возможно. Но в то время как становление и неудача Акки чаще всего достаточно для переключения поведения актеров с несколькими состояниями, построение конечного автомата с более чем несколькими состояниями быстро делает код трудным для рассуждения (и еще труднее для отладки).

Неудивительно, что популярная рекомендация — переходить на Akka FSM, если в нашем актере более двух состояний .

Что такое Акка ФСМ

Чтобы расширить его, FSM Akka — это подход Akka к созданию конечных автоматов, который упрощает управление поведением субъекта в различных состояниях и переходах между этими состояниями.

Под капотом Akka FSM — просто черта, которая расширяет Actor.

1
trait FSM[S, D] extends Actor with Listeners with ActorLogging

Эта черта FSM обеспечивает чистую магию — она ​​предоставляет DSL, который оборачивает обычный Actor, позволяя нам быстрее сосредоточиться на создании конечного автомата, который у нас в руках.

Другими словами, у нашего обычного Actor есть только одна функция receive и свойство FSM оборачивает сложную реализацию метода receive который делегирует вызовы блоку кода, который обрабатывает данные в определенном состоянии.

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

Хорошо, давайте перейдем к коду. Как я уже сказал, мы будем строить кофейный автомат с использованием Akka FSM. State Machine выглядит так:

CoffeeMachineFSM

Состояние и данные

С любым автоматом FSM в любой момент в автомат FSM вовлечены две вещи: состояние машины в любой момент времени и Data которые передаются между штатами. В Akka FSM, чтобы проверить, какие у нас есть Данные, а какие государства, нам нужно только проверить их декларацию.

1
class CoffeeMachine extends FSM[MachineState, MachineData]

Это просто означает, что все состояния FSM простираются от MachineState а данные, которыми обмениваются эти различные состояния, являются просто MachineData .

Как стиль, как и в случае обычного Actor, где мы объявляем все наши сообщения в объекте-компаньоне, мы объявляем наши состояния и данные в объекте-компаньоне:

01
02
03
04
05
06
07
08
09
10
object CoffeeMachine {
 
  sealed trait MachineState
  case object Open extends MachineState
  case object ReadyToBuy extends MachineState
  case object PoweredOff extends MachineState
 
  case class MachineData(currentTxTotal: Int, costOfCoffee: Int, coffeesLeft: Int)
 
}

Итак, как мы зафиксировали на диаграмме ReadyToBuy PoweredOff , у нас есть три состояния — Open , ReadyToBuy и PoweredOff . По нашим данным, MachineData хранит (в обратном порядке) количество кофе, которое торговый автомат может coffeesLeft перед тем, как выключится ( coffeesLeft ), цену каждой чашки кофе ( costOfCoffee ) и, наконец, сумму, costOfCoffee торговым автоматом. пользователь ( currentTxTotal ) — если это меньше, чем стоимость кофе, машина не раздает кофе, если оно больше, то мы должны вернуть остаток наличными.

Вот и все. Мы сделали с государствами и данными.

Прежде чем мы перейдем к реализации каждого из состояний, в которых может находиться торговый автомат, и различных взаимодействий, которые пользователь может иметь с автоматом в определенном состоянии, мы рассмотрим сам актер FSM с высоты 50 000 футов.

Структура ФСМ Актер

Структура FSM Actor выглядит очень похоже на саму диаграмму конечного автомата и выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class CoffeeMachine extends FSM[MachineState, MachineData] {
 
  //What State and Data must this FSM start with (duh!)
  startWith(Open, MachineData(..))
 
  //Handlers of State
  when(Open) {
  ...
  ...
 
  when(ReadyToBuy) {
  ...
  ...
 
  when(PoweredOff) {
  ...
  ...
 
  //fallback handler when an Event is unhandled by none of the States.
  whenUnhandled {
  ...
  ...
 
  //Do we need to do something when there is a State change?
  onTransition {
    case Open -> ReadyToBuy => ...
  ...
  ...
}

Что мы понимаем из структуры:

1) У нас есть начальное состояние (которое Open ), и любые сообщения, которые отправляются на компьютер во время Open состояния, обрабатываются в блоке when(Open) , состояние ReadyToBuy обрабатывается в блоке when(ReadyToBuy) и так далее. Сообщения, на которые я ссылаюсь здесь, похожи на обычные сообщения, которые мы сообщаем простому Actor, за исключением того, что в случае с FSM сообщение также переносится вместе с данными. akka.actor.FSM.Event называется Event ( akka.actor.FSM.Event ), и пример будет выглядеть как Event(deposit: Deposit, MachineData(currentTxTotal, costOfCoffee, coffeesLeft))

Из документации Акки:

1
2
3
4
5
/**
   * All messages sent to the [[akka.actor.FSM]] will be wrapped inside an
   * `Event`, which allows pattern matching to extract both state and data.
   */
  case class Event[D](event: Any, stateData: D) extends NoSerializationVerificationNeeded

2) Мы также замечаем, что функция when принимает два обязательных параметра, первый из которых является именем самого государства, например. Open , ReadyToBuy т. Д., И второй аргумент — это PartialFunction, точно так же, как и при получении Actor, где мы выполняем сопоставление с образцом Здесь важно отметить, что каждый из этих блоков соответствия шаблону должен возвращать состояние (подробнее об этом в следующем посте) . Таким образом, блок кода будет выглядеть примерно так

1
2
3
4
when(Open) { 
    case Event(deposit: Deposit, MachineData(currentTxTotal, costOfCoffee, coffeesLeft)) => {
    ...
    ...

3) Как правило, только те сообщения, которые соответствуют шаблонам, объявленным внутри второго аргумента when обрабатываются в конкретном состоянии. Если нет подходящего шаблона, то FSM Actor пытается сопоставить наше сообщение с шаблоном, объявленным в блоке whenUnhandled . В идеале все сообщения, которые являются общими для всех штатов, кодируются в поле «когда не whenUnhandled . (Я не тот, кто предлагает стиль, но, в качестве альтернативы, вы можете объявить меньшие PartialFunctions и составить их, используя andThen если вы хотите повторно использовать сопоставление с образцом в выбранных состояниях)

4) Наконец, есть функция onTransition которая позволяет вам реагировать или получать уведомления об изменениях в состояниях.

Взаимодействие / Сообщения

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

Ради организации я ввел две черты для всех взаимодействий с Машиной. (Просто чтобы обновить, Interaction / Message — это первый элемент, заключенный в Событие вместе с MachineData). В простых старых терминах Actor это эквивалентно сообщению, которое мы отправляем Actors.

1
2
3
4
5
6
object CoffeeProtocol {
 
  trait UserInteraction
  trait VendorInteraction
...
...

VendorInteraction

Давайте также объявим о различных взаимодействиях, которые Продавец может осуществлять с машиной.

1
2
3
4
5
6
case object ShutDownMachine extends VendorInteraction
  case object StartUpMachine extends VendorInteraction
  case class SetCostOfCoffee(price: Int) extends VendorInteraction
  //Sets Maximum number of coffees that the vending machine could dispense
  case class SetNumberOfCoffee(quantity: Int) extends VendorInteraction
  case object GetNumberOfCoffee extends VendorInteraction

Таким образом, Продавец может

  1. запустить и выключить машину
  2. установить цену на кофе и
  3. установите и получите количество кофе, оставшееся в машине.

UserInteraction

1
2
3
4
5
case class Deposit(value: Int) extends UserInteraction
  case class Balance(value: Int) extends UserInteraction
  case object Cancel extends UserInteraction
  case object BrewCoffee extends UserInteraction
  case object GetCostOfCoffee extends UserInteraction

Теперь, для UserInteraction, Пользователь может

  1. внести деньги, чтобы купить кофе
  2. получить дополнительные деньги, если внесенные деньги превышают стоимость кофе
  3. попросите автомат заваривать кофе, если сумма депозита равна или превышает стоимость кофе
  4. отмените транзакцию, прежде чем варить кофе, и верните все внесенные деньги
  5. запросить у машины стоимость кофе.

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

Код

В интересах нетерпеливых весь код доступен на github .

Ссылка: Примечания Akka — конечные автоматы — 1 от нашего партнера JCG Аруна Маниваннана в блоге Rerun.me .