AKKA 笔记 - 有限状态机 -2
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,using和stateData,让我们下面看下。
STAY和GOTO
想法是每一个被阻塞的case都必须返回一个State。这个可以用stay来完成,含义是已经在处理这条消息的最后了(SetCostOfCoffee或GetCostOfCoffee),咖啡机还在用一个状态,在这里是Open状态。
goto, 将状态变为另一个。我们在讨论Deposit时能看到它是怎么做的。
没啥奇怪的,看下stay方法的实现:
final def stay(): State = goto(currentState.stateName)
USING
你可能已经猜到了,using方法可以让我们把改过的数据传给下个状态。在SetCostOfCoffee消息的例子里,我们设置了MachineData的costOfCoffee域。由于状态是个用例的例子(强烈建议使用不可变除非你喜欢debug),我们做了个copy。
状态数据STATEDATA
stateData是一个我们用来操作FSM数据的方法,就是MachineData。 所以,以下代码块是等价的
case Event(GetCostOfCoffee, _) => sender ! (stateData.costOfCoffee); stay()
case Event(GetCostOfCoffee, machineData) => sender ! (machineData.costOfCoffee); stay()
用GetNumberOfCoffee和SetNumberOfCoffee设置最大咖啡数的实现几乎与设置价格的方法差不多。我们先跳过这个来到更有趣的部分 - 买咖啡。
买咖啡
当咖啡爱好者为咖啡交了钱,我们还不能让咖啡机做咖啡,要等到得到了一杯咖啡的钱才行。而且如果多给了现金,我们还要找零钱,所以,例子会变成这样:
- 直到用户开始存钱了,我们开始追踪他的存款并stay在Open状态。
2.当现金数达到一杯咖啡的钱了,我们会转移成ReadyToBuy状态并允许他买咖啡。 - 在ReadyToBuy状态,他可以改变主意Cancel取消这次交易并拿到所有的退款Balance。
- 如果他想要喝咖啡,它发给咖啡机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元钱。 我们之后假定机器状态从Open到ReadyToBuy,这可以通过接受一条Transition消息来证明咖啡机状态的变更。在第一个例子,转换是从Open到ReadyToBuy。
下一步我们让凯飞机BrewCoffee煮咖啡,这时应该会有一条转换,ReadToBuy到Open。 最终我们断言咖啡机中的数量(就是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的更多相关文章
- [翻译]AKKA笔记 - 有限状态机 -1
原文地址:http://rerun.me/2016/05/21/akka-notes-finite-state-machines-1/ 我最近有个机会在工作上使用了Akka FSM,是个非常有趣的例子 ...
- 翻译:AKKA笔记 - Actor消息 -1(一)
从第一篇Akka笔记的介绍中,我们是从很高的高度去观察Akka工具箱中的Actors.在这篇笔记的第二篇,我们会看一下Actors中的消息部分.而且延续上一次的例子,我们还会使用同样的学生与老师的例子 ...
- 翻译:AKKA笔记 - 介绍Actors
任何以前做过多线程的人都不会否认管理多线程程序是困难并且痛苦的. 我说管理是因为它开始很容易而且当你看到性能提升时会很兴奋.但是,当你看到你没法从子线程的错误中恢复 或者 这些僵尸bug很难重现 或者 ...
- [翻译] AKKA笔记- ACTORSYSTEM (配置CONFIGURATION 与调度SCHEDULING) - 4(一)
原文在http://rerun.me/2014/10/06/akka-notes-actorsystem-in-progress/ 像我们前面看到的,我们可以用ActorSystem的actorof方 ...
- [翻译]AKKA笔记 -ACTOR SUPERVISION - 8
失败更像是分布式系统的一个特性.因此Akka用一个容忍失败的模型,在你的业务逻辑与失败处理逻辑(supervision逻辑)中间你能有一个清晰的边界.只需要一点点工作,这很赞.这就是我们要讨论的主题. ...
- [翻译]AKKA笔记 - DEATHWATCH -7
当我们说Actor生命周期的时候,我们能看到Actor能被很多种方式停掉(用ActorSystem.stop或ActorContext.stop或发送一个PoisonPill - 也有一个kill和g ...
- [翻译]AKKA笔记 - CHILD ACTORS与ACTORPATH -6
原文:http://rerun.me/2014/10/21/akka-notes-child-actors-and-path/ Actor是完全的继承结构.你创建的任何Actor肯定都是一个其他Act ...
- [翻译]AKKA笔记 - ACTOR生命周期 - 基本 -5
原文地址:http://rerun.me/2014/10/21/akka-notes-actor-lifecycle-basic/ (请注意这了讨论的生命周期并不包括 preRestart 或者pos ...
- [翻译]AKKA笔记 - ACTOR MESSAGING - REQUEST AND RESPONSE -3
上次我们看Actor消息机制,我们看到开火-忘记型消息发出(意思是我们只要发个消息给Actor但是不期望有响应). 技术上来讲, 我们发消息给Actors就是要它的副作用. 这就是这么设计的.除了不响 ...
随机推荐
- 一起学微软Power BI系列-使用技巧(4)Power BI中国版企业环境搭建和帐号问题
千呼万唤的Power BI中国版终于落地了,相信12月初的微软技术大会之后已经铺天盖地的新闻出现了,不错,Power BI中国版真的来了,但还有些遗憾,国际版的一些重量级服务如power bi emb ...
- 按需加载.js .css文件
首先,理解按需加载当你需要用到某个js里面的函数什么鬼,或者某个css里的样式的时候你才开始加载这个文件. 然后是怎样实现的,简单来说就是在js中动态的createElem<script> ...
- jQuery.Ajax IE8 无效(CORS)
今天在开发的时候,遇到一个问题,$.get()在 IE8 浏览器不起作用,但 Chrome,Firefox 却是可以的,网上资料很多,最后发现是 IE8 默认不支持 CORS 请求,需要手动开启下: ...
- Canvas讲解
1.Canvas是什么? 简单地说canvas是画布,可以进行画任何的线.图形.填充等一系列的操作,而且操作的画图就是js, 提供简单的二维矢量绘图. 2.步骤: <canvas id=&quo ...
- 深入理解DOM节点操作
× 目录 [1]创建节点 [2]插入节点 [3]移除节点[4]替换节点[5]复制节点 前面的话 一般地,提起操作会想到“增删改查”这四个字,而DOM节点操作也类似地对应于此,接下来将详细介绍DOM的节 ...
- SQL Server 2014聚集列存储索引
转发请注明引用和原文博客(http://www.cnblogs.com/wenBlog) 简介 之前已经写过两篇介绍列存储索引的文章,但是只有非聚集列存储索引,今天再来简单介绍一下聚集的列存储索引,也 ...
- Linux网络驱动--snull
snull是<Linux Device Drivers>中的一个网络驱动的例子.这里引用这个例子学习Linux网络驱动. 因为snull的源码,网上已经更新到适合最新内核,而我自己用的还是 ...
- Object是什么
Object是什么 .Net程序员们每天都在和Object在打交道如果你问一个.Net程序员什么是Object,他可能会信誓旦旦的告诉你"Object还不简单吗,就是所有类型的基类" ...
- CYQ.Data 从入门到放弃ORM系列:开篇:自动化框架编程思维
前言: 随着CYQ.Data 开始回归免费使用之后,发现用户的情绪越来越激动,为了保持这持续的激动性,让我有了开源的念头. 同时,由于框架经过这5-6年来的不断演进,以前发的早期教程已经太落后了,包括 ...
- fedora上部署ASP.NET——(卡带式电脑跑.NET WEB服务器)
andrew,20130601,guilin 本文记录在树莓派(fedora)上部署ASP.NET MVC2 的过程. 本文共分为六部分,分别是前置条件,Apache的安装,Mysql的安装,安装mo ...