最近,我有机会在工作中与Akka FSM一起使用了一些非常有趣的用例。 API(实际上是DSL)非常棒,整个体验令人赞叹。 这是我尝试记录有关使用Akka FSM构建有限状态机的注释。 例如,我们将逐步完成构建(有限)咖啡自动售货机的步骤。
为什么不
我们知道,普通的香草Akka演员可以使用“变得/变得不受欢迎”来切换其行为。 那么,为什么我们需要Akka FSM? 简单的Actor不能只是在州之间切换并表现出不同的行为吗? 是的,可以。 但是,尽管Akka的成败变得通常足以切换涉及几个状态的Actor的行为,但是构建具有多个状态的State Machine很快会使代码难以推理(甚至更难以调试)。
毫不奇怪,如果我们的演员中有两个以上的州 ,流行的建议是切换到Akka FSM。
什么是Akka FSM
为了进一步扩展,Akka FSM是Akka的构建有限状态机的方法,它简化了Actor在各种状态以及这些状态之间的转换的行为的管理。
在后台,Akka FSM只是Actor扩展的特征。
trait FSM[S, D] extends Actor with Listeners with ActorLogging
这种FSM
特性所提供的是纯粹的魔力-它提供了一个包裹常规Actor的DSL,使我们能够集中精力更快地构建手中的状态机。
换句话说,我们的常规Actor只有一个receive
函数,而FSM特征包装了receive
方法的复杂实现,该实现将调用委派给在特定状态下处理数据的代码块。
我个人注意到的另一件好事是,编写后,完整的FSM Actor仍然看起来干净且易于阅读。
好了,让我们看一下代码。 就像我说的,我们将使用Akka FSM制造一台咖啡自动售货机。 状态机看起来像这样:
状态和数据
对于任何FSM,有一个FSM在任何时候涉及到两个东西- State
机器在任何时间点的Data
是在各州之间共享。 在Akka FSM中,为了检查哪些是我们的数据以及哪些是州,我们要做的就是检查其声明。
class CoffeeMachine extends FSM[MachineState, MachineData]
这只是意味着FSM的所有状态都从MachineState
扩展而来,在这些不同状态之间共享的数据只是MachineData
。
就样式而言,就像普通的Actor一样,我们在同一个对象中声明所有消息,我们在同一个对象中声明State和Data:
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)
}
因此,当我们在状态机图捕获,我们有三个国家- Open
, ReadyToBuy
和PoweredOff
。 我们的数据( MachineData
(以相反的顺序保存)自动售货机在关闭之前可以分配的咖啡数量( coffeesLeft
),每杯咖啡的价格( costOfCoffee
),最后是自动售货机存入的金额用户( currentTxTotal
)–如果它少于咖啡的成本,则机器不分配咖啡,如果超过,则我们应退还余额现金。
而已。 我们已经完成了国家和数据。
在我们完成自动售货机可以进入的每个州的实施以及用户在特定州与机器的各种交互之前,我们将对FSM Actor本身有50,000英尺的视野。
FSM Actor的结构
FSM Actor的结构看起来非常类似于状态机图本身,并且看起来像这样:
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)
块中处理,依此类推。 我在这里引用的消息就像我们tell
普通Actor的常规消息一样,除了在FSM的情况下,消息还与数据一起包装。 包装器称为Event
( akka.actor.FSM.Event
),示例类似于Event(deposit: Deposit, MachineData(currentTxTotal, costOfCoffee, coffeesLeft))
从Akka文档中:
/**
* 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的receive
在其中进行模式匹配一样。 这里要注意的最重要的一点是,每个与模式匹配的case
块都必须返回一个State (在下一篇文章中将对此进行更多介绍) 。 因此,代码块看起来像
when(Open) {
case Event(deposit: Deposit, MachineData(currentTxTotal, costOfCoffee, coffeesLeft)) => {
...
...
3)通常,只有与在when
的第二个参数内声明的模式匹配的消息才在特定状态下处理。 如果没有匹配的模式,则FSM Actor尝试将我们的消息与whenUnhandled
块中声明的模式匹配。 理想情况下,所有州之间通用的所有消息都将被编码在whenUnhandled
。 (我不是建议样式的人,但是,您可以声明较小的PartialFunctions并使用andThen
组合它们,如果您想在选定的州之间重复使用模式匹配,则可以)
4)最后,有一个onTransition
函数,可让您对状态变化做出反应或得到通知。
互动/消息
与这种自动售货机互动的人有两种:需要咖啡的咖啡饮用者和负责该机器的管理任务的供应商。
为了便于组织,我为与机器的所有交互引入了两个特征。 (仅刷新一下,“交互/消息”是包装在事件内的第一个元素以及MachineData)。 用简单的旧Actor术语来说,这等同于我们发送给Actor的消息。
object CoffeeProtocol {
trait UserInteraction
trait VendorInteraction
...
...
供应商互动
我们还要声明供应商可以与机器进行的各种交互。
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
因此,供应商可以
- 启动和关闭机器
- 设定咖啡的价格,
- 设置并获取咖啡机中剩余的咖啡数量。
用户互动
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上找到 。
翻译自: https://www.javacodegeeks.com/2016/05/akka-notes-finite-state-machines-1.html