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

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

总结

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

状态和数据

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

object CoffeeMachine {sealed trait MachineStatecase object Open extends MachineStatecase object ReadyToBuy extends MachineStatecase object PoweredOff extends MachineStatecase class MachineData(currentTxTotal: Int, costOfCoffee: Int, coffeesLeft: Int)}

消息

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

object CoffeeProtocol {trait UserInteractiontrait VendorInteractioncase class   Deposit(value: Int) extends UserInteractioncase class   Balance(value: Int) extends UserInteractioncase object  Cancel extends UserInteractioncase object  BrewCoffee extends UserInteractioncase object  GetCostOfCoffee extends UserInteractioncase object  ShutDownMachine extends VendorInteractioncase object  StartUpMachine extends VendorInteractioncase class   SetNumberOfCoffee(quantity: Int) extends VendorInteractioncase class   SetCostOfCoffee(price: Int) extends VendorInteractioncase object  GetNumberOfCoffee extends VendorInteractioncase 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 Statewhen(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 ! GetCostOfCoffeeexpectMsg(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 ! GetNumberOfCoffeeexpectMsg(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 + valuestay 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 ! BrewCoffeeexpectMsg(Transition(coffeeMachine, ReadyToBuy, Open))coffeeMachine ! GetNumberOfCoffeeexpectMsg(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 ! BrewCoffeeexpectMsgPF(){case Balance(value)=>value==1}expectMsg(Transition(coffeeMachine, ReadyToBuy, Open))coffeeMachine ! GetNumberOfCoffeeexpectMsg(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 - costOfCoffeelogger.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 ! CancelexpectMsgPF(){case Balance(value)=>value==6}expectMsg(Transition(coffeeMachine, ReadyToBuy, Open))coffeeMachine ! GetNumberOfCoffeeexpectMsg(10)}

代码

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

像之前一样,代码在github


文章来自微信平台「麦芽面包」
微信公众号「darkjune_think」转载请注明。
如果觉得有趣,微信扫一扫关注公众号。

AKKA 笔记 - 有限状态机 -2相关推荐

  1. Akka笔记–演员介绍

    过去做过多线程的任何人都不会否认管理多线程应用程序有多么艰辛和痛苦. 我说管理是因为它一开始很简单,一旦您开始看到性能改进,它就会变得非常有趣. 但是,当您发现没有一种简单的方法可以从子任务中的错误或 ...

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

    消息 我们只是让QuoteRequest到ActorRef去但是我们根本没见过消息类! 它是这样的: (一个最佳实践是把你的消息类包装在一个完整的对象里以利于更好的组织) TeacherProtoco ...

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

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

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

    原文地址 http://rerun.me/2014/10/06/akka-notes-actorsystem-in-progress/ 2. SCHEDULE 可以看到在ActorSystem的API ...

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

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

  6. java actor_Akka笔记之Actor简介

    英文原文链接,译文链接,原文作者:Arun Manivannan ,译者:有孚 写过多线程的人都不会否认,多线程应用的维护是件多么困难和痛苦的事.我说的是维护,这是因为开始的时候还很简单,一旦你看到性 ...

  7. 《Akka应用模式:分布式应用程序设计实践指南》读书笔记1

    作者属于Scala.Akka技术爱好者,但苦于Akka没有关于设计模式的文章,偶尔搜到<Akka应用模式>一书,如获至宝.现整理一些读书笔记和自己的感悟,以供参考. Actor模型 Act ...

  8. (转)Akka学习笔记

    Akka学习笔记系列文章: <Akka学习笔记:ACTORS介绍> <Akka学习笔记:Actor消息传递(1)> <Akka学习笔记:Actor消息传递(2)> ...

  9. akka的介绍_Akka笔记–演员介绍

    akka的介绍 过去做过多线程的任何人都不会否认管理多线程应用程序有多么艰辛和痛苦. 我说管理是因为它开始很简单,一旦您开始看到性能改进,它就会变得非常有趣. 但是,当您发现没有一种简单的方法可以从子 ...

最新文章

  1. HDFS文件读写流程
  2. java B2B2C源码电子商务平台 -commonservice-config配置服务搭建
  3. 数据预处理常用技巧 | 数据分析中如何处理缺失值?(文末福利)
  4. 仙剑奇侠传 游戏 开发 教程 Xianjian qixia development Game development tutorial
  5. 分布式任务队列 Celery — Overview
  6. 现在c++都转go了
  7. 关于21年电赛,这些一定要熟悉!
  8. 茶百科 android 论文,基于android平台手机茶百科开发_学位论文.doc
  9. 睡觉时钱被转走、开房信息被叫卖、数字货币被篡改,你的安全感,还在吗?...
  10. Django实现发邮件
  11. 从荣耀小米扎堆“滑盖全面屏”,看国产手机的“取巧”式创新
  12. 2路归并排序算法c语言,用二路归并排序算法实现N个元素的排序
  13. python下载付费文档教程-用Python批量爬取付费vip数据,竟然如此简单
  14. vega56刷64_vega56刷vega64_vega56和1070ti_vega56功耗-太平洋电脑网
  15. 解决端口占用问题 Port xxxx was already in use
  16. 回顾2018,生活与代码已无法分离
  17. 【信号完整性】信号反射原理
  18. 期货交易品种基本面分析(期货品种技术面分析)
  19. 很搞笑,今天才弄清楚什么是二级域名和三级域名的区别
  20. 计算机二级office无法评分,计算机二级OFFICE评分标准

热门文章

  1. php语法介绍,PHP语法介绍
  2. leetcode 第2高的薪水 oracle_詹姆斯本赛季薪水3744万美元排在第6位,比他高的都有谁?...
  3. python自加1_python中有自增
  4. 奇异值分解 本质矩阵_Singular Value Decomposition(奇异值分解)
  5. 计算机调剂名额多的考研学校,避免调剂被刷,2020年考研调剂最容易成功的4类院校,提前了解!...
  6. 使用JAVA如何对图片进行格式检查以及安全检查处理
  7. 运行报错Error starting ApplicationContext
  8. Python学习笔记:闭包与作用域
  9. 【codevs2333】【BZOJ2002】弹飞绵羊,第一次的LCT
  10. 2017.9.17 选数 失败总结