文章出處

原文地址: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」轉載請注明。
如果覺得有趣,微信掃一掃關注公眾號。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 AutoPoster 的頭像
    AutoPoster

    互聯網 - 大數據

    AutoPoster 發表在 痞客邦 留言(0) 人氣()