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

我最近有个机会在工作上使用了Akka FSM,是个非常有趣的例子。API(实际上就是DSL),使用体验很棒。这里是我尝试用Akka FSM的有限状态机来写日志。作为例子,我们会以构建一个咖啡机的步骤作为例子。

为什么不用BECOMEUNBECOME

我们知道plain vanilla Akka Actor可以用become/unbecome切换行为。那么,为什么我们需要Akka FSM?不能简单点用Actor在状态间切换? 当然可以。但是当Akka的become and unbecome被一堆状态搅在一起并不停地切换状态的时候,建一个有许多状态的状态机能让代码迅速变的异常难懂(并且难调试)。

没啥奇怪的,常见的建议就是当你在Actor时使用超过2种状态就切换到Akka FSM。

AKKA FSM是啥

Akka FSM是Akka用来简化管理Actor中不同状态和切换状态而构建有限状态机的方法。

在底层,Akka FSM就是一个继承了Actor的trait。

trait FSM[S, D] extends Actor with Listeners with ActorLogging

FSM trait提供的是纯魔法 - 他提供了一个包装了常规Actor的DSL,让我们能集中注意力在更快的构建手头的状态机上。

换句话说,我们的常规Actor只有一个receive方法,FSM trait包装了receive方法的实现并将调用指向到一个特定状态机的处理代码块。

在我写完代码后注意的另一个事,就是完整的FSM Actor仍然很干净并易懂。


现在让我们开始看代码。之前说过,我们要用Akka FSM建一个咖啡机。状态机是这样的:

状态和数据

在FSM中,有两个东西是一直存在的 - 任何时间点都有状态 ,和在状态中进行共享的数据。 在Akka FSM,想要校验哪个是自己的数据,哪个是状态机的数据,我们只要检查这个声明。

class CoffeeMachine extends FSM[MachineState, MachineData]

这代表所有的fsm的状态继承自MachineState,而所有在状态间共享的数据就是MachineData

作为一种风格,跟普通Actor一样我们在companion对象中声明所有的消息,所以我们在companion对象中声明了状态和数据:

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

在状态机的图中,我们有三个状态 - 打开,可买和关闭。 我们的数据,MachineData保留了开飞机关闭前机器中咖啡的数量(coffeesLeft),每杯咖啡的价格(costOfCoffee),咖啡机存放的零钱(currentTxTotal) - 如果零钱比咖啡价格低,机器就不卖咖啡,如果多,那么我们能找回零钱。

关于状态和数据就这么多了。

在我们看每个状态机的实现和用户可用状态机做的交互前, 我们先在5万英尺看下FSM Actor。

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),when(open)代码块处理Open状态的

收到的消息,ReadyToBuy状态由when(ReadyToBuy)代码块来处理。我提到的消息与常规我们发给Actor的消息时一样的,消息与数据一起包装过。包装后的叫做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块必须返回一个状态(下次会讲)。所以,代码块会是这样的

when(Open) {
case Event(deposit: Deposit, MachineData(currentTxTotal, costOfCoffee, coffeesLeft)) => {
...
...

3)基本上, 消息中匹配到了when中第二个参数的模式会被一个特定状态来处理。如果没有匹配到,FSM Actor会尝试将我们的消息与whenUnhandled块中的模式进行匹配。理论上,所有在模式中没有匹配到的消息都会被whenUnhandled处理。(我倒不太想建议编码风格不过你可以声明小点的PartialFunction并用andThen组合使用它,这样你就能在选好的状态中重用模式匹配。)

4)最后,还有个onTransition方法能让你在状态变化时做出反应或得到通知。

交互/消息

会有两类人与咖啡机交互,喝咖啡的人,需要咖啡和咖啡机,和维护咖啡机做管理工作的人。

为了便于管理,所有与机器的交互里我用了两个trait。(再提一下,一个交互/消息是与MachineData一起并被包在Event中的第一个元素。在原来的老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

所以,供应商可以

  1. 打开或关闭机器
  2. 设置咖啡的价格
  3. 设置和拿到机器中已有咖啡的数量。

用户交互

  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

那么,对于用户交互, 用户可以

  1. 存钱买一杯咖啡
  2. 如果钱比咖啡的价格高那么可以得到找零
  3. 如果存的钱正好或高于咖啡价格机器就可以让咖啡机做咖啡
  4. 在煮咖啡前取消交易过程并拿到所有的退款
  5. 问机器查询咖啡的价格

下一篇,我们会看下所有的状态并研究下他们的交互。

代码

代码在github.


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

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

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

[翻译]AKKA笔记 - 有限状态机 -1的更多相关文章

  1. AKKA 笔记 - 有限状态机 -2

    AKKA 笔记 - 有限状态机 -2 原文地址: http://rerun.me/2016/05/22/akka-notes-finite-state-machines-2/ 在上一节的Akka FS ...

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

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

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

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

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

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

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

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

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

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

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

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

  8. [翻译]AKKA笔记 - LOGGING与测试ACTORS -2 (二)

    3.THROW IN A LOGBACK.XML 现在我们把SLF4J日志配置在logback. <?xml version="1.0" encoding="UTF ...

  9. [翻译]AKKA笔记 - LOGGING与测试ACTORS -2 (一)

    在前两章 ( 一 , 二 ) ,我们大致讲了Actor和message是怎么工作的,让我们看一下日志和测试我们的 TeacherActor . RECAP 这是上一节我们的Actor代码: class ...

随机推荐

  1. 【.net 深呼吸】细说CodeDom(4):类型定义

    上一篇文章中说了命名空间,你猜猜接下来该说啥.是了,命名空间下面就是类型,知道了如何生成命名空间的定义代码,之后就该学会如何声明类型了. CLR的类型通常有这么几种:类.接口.结构.枚举.委托.是这么 ...

  2. 前端框架 EasyUI (1)熟悉一下EasyUI

    jQuery EasyUI 官方网站 http://www.jeasyui.com/ .去年新开了个中文网 http://www.jeasyui.net/,不知道是不是官方的,不过看着挺像样.但是,广 ...

  3. Java基础Map接口+Collections工具类

    1.Map中我们主要讲两个接口 HashMap  与   LinkedHashMap (1)其中LinkedHashMap是有序的  怎么存怎么取出来 我们讲一下Map的增删改查功能: /* * Ma ...

  4. Asp.Net MVC中使用StreamReader读取“Post body”之应用场景。

    场景:有三个市场(Global.China.USA),对前台传过来的数据有些验证需要细化到每个市场去完成. 所以就出现了基类(Global)和派生类(China.USA) 定义基类(Global)Pe ...

  5. WPF 有用博客地址

    增加智能感知的RichTextBox扩展控件(WPF) WPF自定义控件与样式(3)-TextBox & RichTextBox & PasswordBox样式.水印.Label标签. ...

  6. 深入理解 Java G1 垃圾收集器--转

    原文地址:http://blog.jobbole.com/109170/?utm_source=hao.jobbole.com&utm_medium=relatedArticle 本文首先简单 ...

  7. Velocity初探小结--velocity使用语法详解

    做java开发的朋友一般对JSP是比较熟悉的,大部分人第一次学习开发View层都是使用JSP来进行页面渲染的,我们都知道JSP是可以嵌入java代码的,在远古时代,java程序员甚至在一个jsp页面上 ...

  8. BPM体系文件管理解决方案分享

    一.方案概述 企业管理在很大程度上是通过文件化的形式表现出来,体系文件管理是管理体系存在的基础和证据,是规范企业管理活动和全体人员行为,达到管理目标的管理依据.对与公司质量.环境.职业健康安全等体系有 ...

  9. Android的Kotlin秘方(I):OnGlobalLayoutListener

    春节后,又重新“开张”.各位高手请继续支持.谢谢! 原文标题:Kotlin recipes for Android (I): OnGlobalLayoutListener 原文链接:http://an ...

  10. GitHub管理代码-随笔

    公司一直用的SVN进行项目管理,平时便自己折腾了下Git,这里做下GitHub的最简单的记录... 在git上创建仓库等就免谈了,网上也有好多教程,直接从创建之后记录: 在github的readme文 ...