在有关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中以一种非常直观的方法startWith
。 startWith
接受两个参数-初始状态和初始数据。
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()
...
...
}
}
上面的代码中有几个新单词– stay
, using
和stateData
。 让我们详细看看它们。
这个想法是,状态中的每个case
块必须返回一个State
。 这可以通过使用stay
来完成,这仅意味着在处理此消息( SetCostOfCoffee
或GetCostOfCoffee
)结束时,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()
使用GetNumberOfCoffee
和SetNumberOfCoffee
分配的最大咖啡数量的实现几乎与设置和获取价格本身相同。 让我们跳过那一步,去有趣的部分-购买咖啡。
买咖啡
因此,咖啡爱好者会为咖啡存钱,但是在他输入咖啡费用之前,我们不允许机器分配咖啡。 另外,如果他给了额外的现金,我们必须给他余额。 因此,各种情况如下:
- 在用户存入一笔钱以支付咖啡费用之前,我们将跟踪他已存入的累计金额并
stay
在“Open
状态。 - 一旦累计现金超过了咖啡的价格,我们将过渡到
ReadyToBuy
状态,并允许他购买咖啡。 - 在此
ReadyToBuy
状态下,他可以改变主意并Cancel
交易,在该交易期间将返还其所有累积Balance
。 - 如果他想喝咖啡,他会向咖啡机发送
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
,该消息会向我们提供有关咖啡机的“ 从”和“至”状态的信息。 在第一种情况下,这是从Open
到ReadyToBuy
的过渡。
然后,我们走到更远的地方,问机器到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