原文地址:http://rerun.me/2016/05/21/akka-notes-finite-state-machines-1/
我最近有個機會在工作上使用了Akka FSM,是個非常有趣的例子。API(實際上就是DSL),使用體驗很棒。這里是我嘗試用Akka FSM的有限狀態機來寫日志。作為例子,我們會以構建一個咖啡機的步驟作為例子。
為什么不用BECOME和UNBECOME
我們知道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
所以,供應商可以
- 打開或關閉機器
- 設置咖啡的價格
- 設置和拿到機器中已有咖啡的數量。
用戶交互
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
那么,對于用戶交互, 用戶可以
- 存錢買一杯咖啡
- 如果錢比咖啡的價格高那么可以得到找零
- 如果存的錢正好或高于咖啡價格機器就可以讓咖啡機做咖啡
- 在煮咖啡前取消交易過程并拿到所有的退款
- 問機器查詢咖啡的價格
下一篇,我們會看下所有的狀態并研究下他們的交互。
代碼
代碼在github.
文章來自微信平臺「麥芽面包」
微信公眾號「darkjune_think」轉載請注明。
如果覺得有趣,微信掃一掃關注公眾號。
文章列表
不含病毒。www.avast.com |
留言列表