Akka笔记–有限状态机– 2

在有关Akka FSM的注释的第一部分中,我们了解了Akka FSM的基础知识以及我们计划制造的咖啡自动售货机的概述– Actor的结构以及传递给Actor的消息列表。 在第二部分(也是最后一部分)中,我们将继续实施这些国家。

概括

快速回顾一下,让我们看一下FSM的结构以及可以发送给它的消息。

状态和数据

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的结构

这是我们在第1部分中看到的总体结构

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中以一种非常直观的方法startWithstartWith接受两个参数-初始状态和初始数据。

class CoffeeMachine extends FSM[MachineState, MachineData] {

  startWith(Open, MachineData(currentTxTotal = 0, costOfCoffee =  5, coffeesLeft = 10))

...
...

上面的代码仅表示FSM的初始状态为Open ,而在Coffee咖啡机Open的初始数据为MachineData(currentTxTotal = 0, costOfCoffee = 5, coffeesLeft = 10)

由于机器刚刚启动,因此自动售货机的启动状态良好。 它尚未与任何用户交互,因此该交易的当前显示余额为0。咖啡的价格设置为5美元,机器总共可以出售的咖啡总数为10。 10杯咖啡,剩下0杯,机器关闭。

实施国家

啊,终于!

我认为,在不同状态下查看与自动售货机交互的最简单方法是对交互进行分组,围绕其编写测试用例以及将其与FSM中的实现一起进行。

如果您指的是github代码,则所有测试都在CoffeeSpec中 ,而FSM是CoffeeMachine

以下所有测试都包装在CoffeeSpec测试类中,该类的声明如下:

class CoffeeSpec extends TestKit(ActorSystem("coffee-system")) with MustMatchers with FunSpecLike with ImplicitSender

设定咖啡价格

如上所述,MachineData的起始价格为每杯咖啡5美元,最多可容纳10杯咖啡。 这只是一个初始状态。 卖方必须有能力在任何时间设定咖啡的价格和咖啡机的容量。

通过将SetCostOfCoffee消息发送给SetCostOfCoffee来实现价格设置。 我们还应该有能力获得咖啡的价格。 这是通过使用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)
    }
...
...
...

实作

就像我们在第1部分中讨论的那样,每条发送到FSM的消息都被接收并包装在Event类中,该类也包装MachineData

when(Open) {
     case Event(SetCostOfCoffee(price), _) => stay using stateData.copy(costOfCoffee = price)
    case Event(GetCostOfCoffee, _) => sender ! (stateData.costOfCoffee); stay()
   ...
   ...
  }
}

上面的代码中有几个新单词– stayusingstateData 。 让我们详细看看它们。

这个想法是,状态中的每个case块必须返回一个State 。 这可以通过使用stay来完成,这仅意味着在处理此消息( SetCostOfCoffeeGetCostOfCoffee )结束时,CoffeeMachine保持相同的状态,在本例中为Open

另一方面, goto转换为其他状态。 我们将在讨论“ Deposit消息时看到它是如何完成的。

毫不奇怪,请检查一下stay函数的实现

final def stay(): State = goto(currentState.stateName)

using

您可能已经猜到了, using函数允许我们将修改后的数据传递到下一个状态。 在该情况下SetCostOfCoffee消息,我们设定costOfCoffee所述的场MachineData到传入price包裹内部SetCostOfCoffee 。 由于State是一个案例类(除非您在奇怪的时间进行有趣的调试,否则强烈建议不可改变性),因此我们进行了copy

stateData

stateData只是一个函数,为我们提供了FSM数据(即MachineData本身)的MachineData 。 因此,以下代码块是等效的

case Event(GetCostOfCoffee, _) => sender ! (stateData.costOfCoffee); stay()
case Event(GetCostOfCoffee, machineData) => sender ! (machineData.costOfCoffee); stay()

使用GetNumberOfCoffeeSetNumberOfCoffee分配的最大咖啡数量的实现几乎与设置和获取价格本身相同。 让我们跳过那一步,去有趣的部分-购买咖啡。

买咖啡

因此,咖啡爱好者会为咖啡存钱,但是在他输入咖啡费用之前,我们不允许机器分配咖啡。 另外,如果他给了额外的现金,我们必须给他余额。 因此,各种情况如下:

  1. 在用户存入一笔钱以支付咖啡费用之前,我们将跟踪他已存入的累计金额并stay在“ Open状态。
  2. 一旦累计现金超过了咖啡的价格,我们将过渡到ReadyToBuy状态,并允许他购买咖啡。
  3. 在此ReadyToBuy状态下,他可以改变主意并Cancel交易,在该交易期间将返还其所有累积Balance
  4. 如果他想喝咖啡,他会向咖啡机发送BrewCoffee消息,在此期间,我们分配咖啡,并将Balance钱还给他。 (实际上,在我们的代码中,我们不分配咖啡。我们只是从他的存款中减去咖啡的价格,然后给他余下的余额。真是可恶!!)

让我们考虑以上每种情况

情况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状态,等待更多的Deposit

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美元,恰好是咖啡的价格。 然后,我们通过ReadyToBuy Transition消息来断定该机器已从“ Open过渡到“ ReadyToBuy ,该消息会向我们提供有关咖啡机的“ 从”至”状态的信息。 在第一种情况下,这是从OpenReadyToBuy的过渡。

然后,我们走到更远的地方,问机器到BrewCoffee在此期间,我们期望在分配咖啡后再次从ReadyToBuy过渡到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 –存入的金额高于咖啡的价格

第二个测试用例与第一个测试用例相似,除了我们以大于咖啡价格(6美元)的增量存入现金外,其90%相似。 由于我们将咖啡的价格设置为5美元,因此我们现在希望获得Balance为1美元的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 ,我们将检查是否有余额要分配。 如果没有,我们只需从咖啡总数中减去一份咖啡就转换为“ Open状态。 否则,我们减去咖啡数量后就分配余额并过渡到“ Open状态。 (就像我之前说过的,在此示例中,我们实际上并未分配咖啡)

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交易。就像我们在第1部分中讨论的那样,保存此类通用消息的理想位置是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消息和PoweredOff状态的解释,但是如果您希望为他们提供解释,请发表评论。

与往常一样,该代码在github上可用。

翻译自: https://www.javacodegeeks.com/2016/05/akka-notes-finite-state-machines-2.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值