Недавно у меня была возможность поиграть с 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 выглядит так:
Состояние и данные
С любым автоматом 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 |
Таким образом, Продавец может
- запустить и выключить машину
- установить цену на кофе и
- установите и получите количество кофе, оставшееся в машине.
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, Пользователь может
- внести деньги, чтобы купить кофе
- получить дополнительные деньги, если внесенные деньги превышают стоимость кофе
- попросите автомат заваривать кофе, если сумма депозита равна или превышает стоимость кофе
- отмените транзакцию, прежде чем варить кофе, и верните все внесенные деньги
- запросить у машины стоимость кофе.
В следующем посте мы рассмотрим каждое из состояний и подробно рассмотрим их взаимодействия (вместе с тестовыми сценариями).
Код
В интересах нетерпеливых весь код доступен на github .
Ссылка: | Примечания Akka — конечные автоматы — 1 от нашего партнера JCG Аруна Маниваннана в блоге Rerun.me . |