AKKA 笔记 - 有限状态机 -2

原文地址: http://rerun.me/2016/05/22/akka-notes-finite-state-machines-2/

在上一节的Akka FSM笔记中,我们看了一些基本的使用Akka FSM和咖啡机的使用方式 - Actor的数据结构和一队我们要发给Actor的消息。这次的第二部分也是最终部分,我们会过一遍这些状态的实现细节。

总结

作为一个快速的总结,让我们先看一下FMS的结构和我们要发过去的消息。

状态和数据

FSM的三个状态和要在各个状态发送的数据是:

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) }

消息

我们发给FSM的咖啡机和用户交互的消息是:

object CoffeeProtocol {

  trait UserInteraction
trait 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 case object ShutDownMachine extends VendorInteraction
case object StartUpMachine extends VendorInteraction
case class SetNumberOfCoffee(quantity: Int) extends VendorInteraction
case class SetCostOfCoffee(price: Int) extends VendorInteraction
case object GetNumberOfCoffee extends VendorInteraction case class MachineError(errorMsg:String) }

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 => ...
...
...
}

状态初始化

跟其他状态机一样, FSM在启动时需要一个初始化状态。这个可以在Akka FSM内声明一个叫startWith的方法来实现。startWith接受两个参数 - 初始化状态和初始化数据。

class CoffeeMachine extends FSM[MachineState, MachineData] {

  startWith(Open, MachineData(currentTxTotal = 0, costOfCoffee =  5, coffeesLeft = 10))

...
...

以上代码说明了FSM的初始化状态是Open并且当咖啡机Open时的初始化数据是

MachineData(currentTxTotal = 0, costOfCoffee = 5, coffeesLeft = 10).

当机器启动时,咖啡机是一个干净的状态。它跟用户还没有任何交互,当前的余额是0。咖啡的价格呗设置成5元,总共能提供的咖啡设置为10杯。当咖啡机冲了10杯咖啡后数量为0时,咖啡机会shut down。

状态的实现

终于到最后了!!

我觉得最简单的方式来看咖啡机状态的交互就是给交互做个分组,为FSM的实现写测试用例。

如果你看下github的代码,所有的测试用例都在CoffeeSpec并且FSM在CoffeeMachine

以下所有的测试都被CoffeeSpec测试类包装了,声明就像这样:

class CoffeeSpec extends TestKit(ActorSystem("coffee-system")) with MustMatchers with FunSpecLike with ImplicitSender

设置并得到咖啡的价格

像我们之前看到的,MachineData初始化时设置为每杯咖啡5元并总数为10杯。这只是一个初始状态,咖啡机必须能在任何时候设置咖啡的价格和能提供的数量。

通过发送SetCostOfCoffee消息给Actor可以设置价格。我们也应该能拿到咖啡的价格。这个可以通过发送GetCostOfCoffee消息给机器来获得。

测试用例

describe("The Coffee Machine") {

   it("should allow setting and getting of price of coffee") {
val coffeeMachine = TestActorRef(Props(new CoffeeMachine()))
coffeeMachine ! SetCostOfCoffee(7)
coffeeMachine ! GetCostOfCoffee
expectMsg(7)
}
...
...
...

实现

像我们在第一节讨论的,所有发给FSM的消息都被包装成Event类,并且也被MachineData包装:

 when(Open) {
case Event(SetCostOfCoffee(price), _) => stay using stateData.copy(costOfCoffee = price)
case Event(GetCostOfCoffee, _) => sender ! (stateData.costOfCoffee); stay()
...
...
}
}

以上代码有几个新词 - stay,usingstateData,让我们下面看下。

STAYGOTO

想法是每一个被阻塞的case都必须返回一个State。这个可以用stay来完成,含义是已经在处理这条消息的最后了(SetCostOfCoffeeGetCostOfCoffee),咖啡机还在用一个状态,在这里是Open状态。

goto, 将状态变为另一个。我们在讨论Deposit时能看到它是怎么做的。

没啥奇怪的,看下stay方法的实现:

  final def stay(): State = goto(currentState.stateName)

USING

你可能已经猜到了,using方法可以让我们把改过的数据传给下个状态。在SetCostOfCoffee消息的例子里,我们设置了MachineDatacostOfCoffee域。由于状态是个用例的例子(强烈建议使用不可变除非你喜欢debug),我们做了个copy

状态数据STATEDATA

stateData是一个我们用来操作FSM数据的方法,就是MachineData。 所以,以下代码块是等价的

case Event(GetCostOfCoffee, _) => sender ! (stateData.costOfCoffee); stay()
case Event(GetCostOfCoffee, machineData) => sender ! (machineData.costOfCoffee); stay()

GetNumberOfCoffeeSetNumberOfCoffee设置最大咖啡数的实现几乎与设置价格的方法差不多。我们先跳过这个来到更有趣的部分 - 买咖啡。

买咖啡

当咖啡爱好者为咖啡交了钱,我们还不能让咖啡机做咖啡,要等到得到了一杯咖啡的钱才行。而且如果多给了现金,我们还要找零钱,所以,例子会变成这样:

  1. 直到用户开始存钱了,我们开始追踪他的存款并stayOpen状态。

    2.当现金数达到一杯咖啡的钱了,我们会转移成ReadyToBuy状态并允许他买咖啡。
  2. ReadyToBuy状态,他可以改变主意Cancel取消这次交易并拿到所有的退款Balance
  3. 如果他想要喝咖啡,它发给咖啡机BrewCoffee煮咖啡的消息。(事实上,我们的代码里并不会分发咖啡。我们只是从用户的存款里减掉了咖啡的价格并找零。)

让我们看下以下的用例

用例1 用户存钱单但存的钱低于咖啡的价格

用例开始设置咖啡的价格为5元并且咖啡总数为10。 我们存2元并检查机器是不是在Open状态并且咖啡总数仍然是10.

 it("should stay at Transacting when the Deposit is less then the price of the coffee") {
val coffeeMachine = TestActorRef(Props(new CoffeeMachine()))
coffeeMachine ! SetCostOfCoffee(5)
coffeeMachine ! SetNumberOfCoffee(10)
coffeeMachine ! SubscribeTransitionCallBack(testActor) expectMsg(CurrentState(coffeeMachine, Open)) coffeeMachine ! Deposit(2) coffeeMachine ! GetNumberOfCoffee expectMsg(10)
}

我们怎样确保机器在Open状态?

每个FSM都能处理一条叫FSM.SubscribeTransitionCallBack(callerActorRef)的特殊消息,能让调用者在任何状态变动时被通知。第一条发给订阅者的通知消息是CurrentState, 告诉我们FSM在哪个状态。 这之后会有若干条Transition消息。

实现

我们继续存钱并维持在Open状态并等待存更多的钱

when(Open) {
...
...
case Event(Deposit(value), MachineData(currentTxTotal, costOfCoffee, coffeesLeft)) if (value + currentTxTotal) < stateData.costOfCoffee => {
val cumulativeValue = currentTxTotal + value
stay using stateData.copy(currentTxTotal = cumulativeValue)
}

用例2和4 - 用户存钱并达到咖啡的价钱

测试用例1 - 存与咖啡价格等值的钱

我们的用例启动机器,确认是否当前状态是Open并存5元钱。 我们之后假定机器状态从OpenReadyToBuy,这可以通过接受一条Transition消息来证明咖啡机状态的变更。在第一个例子,转换是从OpenReadyToBuy

下一步我们让凯飞机BrewCoffee煮咖啡,这时应该会有一条转换,ReadToBuyOpen。 最终我们断言咖啡机中的数量(就是9)。

it("should transition to ReadyToBuy and then Open when the Deposit is equal to the price of the coffee") {
val coffeeMachine = TestActorRef(Props(new CoffeeMachine()))
coffeeMachine ! SetCostOfCoffee(5)
coffeeMachine ! SetNumberOfCoffee(10)
coffeeMachine ! SubscribeTransitionCallBack(testActor) expectMsg(CurrentState(coffeeMachine, Open)) coffeeMachine ! Deposit(5) expectMsg(Transition(coffeeMachine, Open, ReadyToBuy)) coffeeMachine ! BrewCoffee
expectMsg(Transition(coffeeMachine, ReadyToBuy, Open)) coffeeMachine ! GetNumberOfCoffee expectMsg(9)
}

测试用例2 - 存大于咖啡价格的钱

第二个例子跟第一个比有90%一样,除了我们存在钱更多了(是6元)。 因为我们把咖啡价格设为5元, 现在我们期望应该有一块钱的Balance找零消息

it("should transition to ReadyToBuy and then Open when the Deposit is greater than the price of the coffee") {
val coffeeMachine = TestActorRef(Props(new CoffeeMachine()))
coffeeMachine ! SetCostOfCoffee(5)
coffeeMachine ! SetNumberOfCoffee(10)
coffeeMachine ! SubscribeTransitionCallBack(testActor) expectMsg(CurrentState(coffeeMachine, Open)) coffeeMachine ! Deposit(2)
coffeeMachine ! Deposit(2)
coffeeMachine ! Deposit(2) expectMsg(Transition(coffeeMachine, Open, ReadyToBuy)) coffeeMachine ! BrewCoffee expectMsgPF(){
case Balance(value)=>value==1
} expectMsg(Transition(coffeeMachine, ReadyToBuy, Open)) coffeeMachine ! GetNumberOfCoffee expectMsg(9)
}

实现

这个实现比之前的测试用例简单。如果存款大于咖啡价格,那么我们转到goto ReadyToBuy状态。

when(Open){
...
...
case Event(Deposit(value), MachineData(currentTxTotal, costOfCoffee, coffeesLeft)) if (value + currentTxTotal) >= stateData.costOfCoffee => {
goto(ReadyToBuy) using stateData.copy(currentTxTotal = currentTxTotal + value)
}

一旦转到ReadyToBuy状态, 当用户发送BrewCoffee,我们检查是否有零钱找零。

  when(ReadyToBuy) {
case Event(BrewCoffee, MachineData(currentTxTotal, costOfCoffee, coffeesLeft)) => {
val balanceToBeDispensed = currentTxTotal - costOfCoffee
logger.debug(s"Balance is $balanceToBeDispensed")
if (balanceToBeDispensed > 0) {
sender ! Balance(value = balanceToBeDispensed)
goto(Open) using stateData.copy(currentTxTotal = 0, coffeesLeft = coffeesLeft - 1)
}
else goto(Open) using stateData.copy(currentTxTotal = 0, coffeesLeft = coffeesLeft - 1)
}
}

用例3 用户要取消交易

实际上, 用户应该可以在交易的任何时间点Cancel取消,无论他在什么状态。我们之前在第一部分讨论过,最好的保存这里通用消息的地方在whenUnhandled代码块。我们要确定用户在取消前是否存了一些钱,我们要还给他们。

实现

  whenUnhandled {
...
...
case Event(Cancel, MachineData(currentTxTotal, _, _)) => {
sender ! Balance(value = currentTxTotal)
goto(Open) using stateData.copy(currentTxTotal = 0)
}
}

测试用例

这个例子跟我们以上看到的差不多,除了找零。

 it("should transition to Open after flushing out all the deposit when the coffee is canceled") {
val coffeeMachine = TestActorRef(Props(new CoffeeMachine()))
coffeeMachine ! SetCostOfCoffee(5)
coffeeMachine ! SetNumberOfCoffee(10)
coffeeMachine ! SubscribeTransitionCallBack(testActor) expectMsg(CurrentState(coffeeMachine, Open)) coffeeMachine ! Deposit(2)
coffeeMachine ! Deposit(2)
coffeeMachine ! Deposit(2) expectMsg(Transition(coffeeMachine, Open, ReadyToBuy)) coffeeMachine ! Cancel expectMsgPF(){
case Balance(value)=>value==6
} expectMsg(Transition(coffeeMachine, ReadyToBuy, Open)) coffeeMachine ! GetNumberOfCoffee expectMsg(10)
}

代码

我不想烦死你所以跳过了解释ShutDownMachine消息和PowerOff状态,如果你想要解释,可以留言。

像之前一样,代码在github


文章来自微信平台「麦芽面包」

微信公众号「darkjune_think」转载请注明。

如果觉得有趣,微信扫一扫关注公众号。

AKKA 笔记 - 有限状态机 -2的更多相关文章

  1. [翻译]AKKA笔记 - 有限状态机 -1

    原文地址:http://rerun.me/2016/05/21/akka-notes-finite-state-machines-1/ 我最近有个机会在工作上使用了Akka FSM,是个非常有趣的例子 ...

  2. 翻译:AKKA笔记 - Actor消息 -1(一)

    从第一篇Akka笔记的介绍中,我们是从很高的高度去观察Akka工具箱中的Actors.在这篇笔记的第二篇,我们会看一下Actors中的消息部分.而且延续上一次的例子,我们还会使用同样的学生与老师的例子 ...

  3. 翻译:AKKA笔记 - 介绍Actors

    任何以前做过多线程的人都不会否认管理多线程程序是困难并且痛苦的. 我说管理是因为它开始很容易而且当你看到性能提升时会很兴奋.但是,当你看到你没法从子线程的错误中恢复 或者 这些僵尸bug很难重现 或者 ...

  4. [翻译] AKKA笔记- ACTORSYSTEM (配置CONFIGURATION 与调度SCHEDULING) - 4(一)

    原文在http://rerun.me/2014/10/06/akka-notes-actorsystem-in-progress/ 像我们前面看到的,我们可以用ActorSystem的actorof方 ...

  5. [翻译]AKKA笔记 -ACTOR SUPERVISION - 8

    失败更像是分布式系统的一个特性.因此Akka用一个容忍失败的模型,在你的业务逻辑与失败处理逻辑(supervision逻辑)中间你能有一个清晰的边界.只需要一点点工作,这很赞.这就是我们要讨论的主题. ...

  6. [翻译]AKKA笔记 - DEATHWATCH -7

    当我们说Actor生命周期的时候,我们能看到Actor能被很多种方式停掉(用ActorSystem.stop或ActorContext.stop或发送一个PoisonPill - 也有一个kill和g ...

  7. [翻译]AKKA笔记 - CHILD ACTORS与ACTORPATH -6

    原文:http://rerun.me/2014/10/21/akka-notes-child-actors-and-path/ Actor是完全的继承结构.你创建的任何Actor肯定都是一个其他Act ...

  8. [翻译]AKKA笔记 - ACTOR生命周期 - 基本 -5

    原文地址:http://rerun.me/2014/10/21/akka-notes-actor-lifecycle-basic/ (请注意这了讨论的生命周期并不包括 preRestart 或者pos ...

  9. [翻译]AKKA笔记 - ACTOR MESSAGING - REQUEST AND RESPONSE -3

    上次我们看Actor消息机制,我们看到开火-忘记型消息发出(意思是我们只要发个消息给Actor但是不期望有响应). 技术上来讲, 我们发消息给Actors就是要它的副作用. 这就是这么设计的.除了不响 ...

随机推荐

  1. 初识Hadoop、Hive

    2016.10.13 20:28 很久没有写随笔了,自打小宝出生后就没有写过新的文章.数次来到博客园,想开始新的学习历程,总是被各种琐事中断.一方面确实是最近的项目工作比较忙,各个集群频繁地上线加多版 ...

  2. 快速构建H5单页面切换骨架

    在Web App和Hybrid App横行的时代,为了拥有更好的用户体验,单页面应用顺势而生,单页面应用简称`SPA`,即Single Page Application,就是只有一个HTML页面的应用 ...

  3. Docker笔记一:基于Docker容器构建并运行 nginx + php + mysql ( mariadb ) 服务环境

    首先为什么要自己编写Dockerfile来构建 nginx.php.mariadb这三个镜像呢?一是希望更深入了解Dockerfile的使用,也就能初步了解docker镜像是如何被构建的:二是希望将来 ...

  4. WPF中Grid实现网格,表格样式通用类

    /// <summary> /// 给Grid添加边框线 /// </summary> /// <param name="grid"></ ...

  5. Flexible 弹性盒子模型之CSS flex-flow

    实例 让弹性盒的元素以相反的顺序显示,且在必要的时候进行拆行: display:flex; flex-flow:row-reverse wrap;   效果预览 浏览器支持 表格中的数字表示支持该属性 ...

  6. 在 SharePoint Server 2016 本地环境中设置 OneDrive for Business

    建议补丁 建议在sharepoint2016打上KB3127940补丁,补丁下载地址 https://support.microsoft.com/zh-cn/kb/3127940 当然不打,也可以用O ...

  7. atitit.attilax的软件 架构 理念.docx

    atitit.attilax的软件 架构 理念.docx 1. 预先规划.1 2. 全体系化1 3. 跨平台2 4. 跨语言2 5. Dsl化2 5.1. 界面ui h5化2 6. 跨架构化2 7. ...

  8. 检查sql执行效率

    SELECT  SUBSTRING(ST.text, ( QS.statement_start_offset / 2 ) + 1,                    ( ( CASE statem ...

  9. python select网络编程详细介绍

    刚看了反应堆模式的原理,特意复习了socket编程,本文主要介绍python的基本socket使用和select使用,主要用于了解socket通信过程 一.socket模块 socket - Low- ...

  10. 数据库 oracle数据库基本知识

    sqlplus登录 普通用户登录 c:\>sqlplus 请输入用户名:scott 请输入口令: sqlplus scott/ quit退出 管理员登录 sqlplus /nolog 连接数据库 ...