Last active
October 28, 2015 02:54
-
-
Save brianhsu/0785868ea6cc1530a140 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import akka.actor._ | |
import akka.testkit._ | |
import org.scalatest._ | |
import scala.concurrent.duration._ | |
object VendingMachine { | |
// 自動販賣機會用到的資料:目前投了多少錢,有什麼產品,使用者選了什麼產品 | |
case class Product(price: Int, stock: Int) | |
case class MoneyAndProducts(currentMoneyInside: Int, products: Map[String, Product], selectedItem: Option[String] = None) | |
// 自動販賣機可能的狀態 | |
sealed trait State | |
case object Idle extends State | |
case object Selection extends State | |
case object Dispending extends State | |
// 自動賦販賣機可以接受的事件 | |
case class InsertCoins(coins: Int) | |
case class Selection(productName: String) | |
case object Refund | |
case object Done | |
// 送回給 UI 的資料 | |
case class SendProduct(item: String) | |
case class Change(coins: Int) | |
case class SelectableProducts(currentMony: Int, items: Map[String, Product]) | |
// 一開始的產品價格和資料 | |
val defaultStocks = Map( | |
"可口可樂" -> Product(25, 100), | |
"雪碧" -> Product(25, 100), | |
"生活紅茶" -> Product(10, 200), | |
"無糖綠茶" -> Product(20, 200), | |
"伯朗咖啡" -> Product(30, 50) | |
) | |
} | |
class VendingMachine extends FSM[VendingMachine.State, VendingMachine.MoneyAndProducts] { | |
import VendingMachine._ | |
// 一開始自動販賣機在 Idle 的狀態,沒有人投錢,商品是滿的 | |
startWith(Idle, MoneyAndProducts(0, defaultStocks)) | |
/** | |
* 檢查使用者投的錢是否夠買某個商品和該商品庫存是否足夠 | |
* | |
* @param item 使用者想買的商品 | |
* @param currentData 目前自動販賣機裡投了多少錢,有什麼商品 | |
* @return 若商品有庫存,而且機器裡的錢也夠則為 true | |
*/ | |
def productIsAavaliable(item: String, currentData: MoneyAndProducts): Boolean = { | |
currentData.products.get(item) match { | |
case Some(Product(price, stock)) if (stock > 0 && currentData.currentMoneyInside >= price) => true | |
case _ => false | |
} | |
} | |
/** | |
* 買了商品之後的新狀態: | |
* | |
* - 商品庫存數量減一 | |
* - 自動販賣機裡的錢為要找零的錢(原本投的金額-商品標價) | |
* | |
* @param currentData 原本的自動販賣機資料 | |
* @param item 要買的商品 | |
* @return 新的自動販賣機資料 | |
*/ | |
def newData(currentData: MoneyAndProducts, itemName: String) = { | |
val newProduct = currentData.products.get(itemName).map(x => x.copy(stock = x.stock -1)).get | |
val newProductList = currentData.products.updated(itemName, newProduct) | |
currentData.copy( | |
currentMoneyInside = currentData.currentMoneyInside - newProduct.price, | |
products = newProductList, | |
selectedItem = Some(itemName) | |
) | |
} | |
// 當自動販賣機在 Idle 狀態 | |
when(Idle) { | |
// 接收到投幣事件時,轉到 Selection 狀態,並且把使用者投的錢加上去 | |
case Event(InsertCoins(inserted), currentData) => | |
val totalMoney = currentData.currentMoneyInside + inserted | |
goto(Selection) using currentData.copy(currentMoneyInside = totalMoney) | |
} | |
// 當在 Selection 狀態 | |
when(Selection) { | |
// 繼續投幣的話就繼續停在 Selection 狀態 | |
case Event(InsertCoins(inserted), currentData) => | |
val totalMoney = currentData.currentMoneyInside + inserted | |
goto(Selection) using currentData.copy(currentMoneyInside = totalMoney) | |
// 按下商品按鈕,而且可以買的話,就轉到出貨狀態 | |
case Event(Selection(item), currentData) if productIsAavaliable(item, currentData) => | |
goto(Dispending) using newData(currentData, item) | |
// 如果按下退幣鈕,那就回到 Idle 的狀態,販賣機的投幣金額應該也要歸零。 | |
case Event(Refund, currentData) => | |
goto(Idle) using currentData.copy(currentMoneyInside = 0) | |
} | |
// 如果是在出貨狀態 | |
when(Dispending) { | |
// 收到出完貨的訊息,那就回到 Idle 等下一次的投幣,已投金額也歸零 | |
case Event(Done, currentData) => goto(Idle) using currentData.copy(currentMoneyInside = 0) | |
} | |
// 從某個狀態轉換到另一個狀態,應該告知 UI 做一些事 | |
onTransition { | |
// 如果是從 Idle 轉到投幣,或使用者持續投幣, | |
// 那要告知使用者現在投了多少錢,可以選什麼商品(錢夠而且有庫存)。 | |
case Idle -> Selection | Selection -> Selection => | |
val MoneyAndProducts(currentMoneyInside, products, _) = nextStateData | |
val avaliableItems = products.filter{case (name, product) => product.price <= currentMoneyInside} | |
sender ! SelectableProducts(currentMoneyInside, avaliableItems) | |
// 如果從 Selection 轉到 Idle(按下退幣) | |
// 那就把原來投的零錢還給使用者 | |
case Selection -> Idle => | |
val MoneyAndProducts(currentMoneyInside, products, _) = stateData | |
sender ! Change(currentMoneyInside) | |
// 如果從 Selection 轉到 Dispending,那就是出貨, | |
// 先把商品送出,然後找零,最後再發出出貨完成的事件給自動販賣機自己。 | |
case Selection -> Dispending => | |
val MoneyAndProducts(newMoneyInside, _, Some(selectedItem)) = nextStateData | |
println(newMoneyInside) | |
sender ! SendProduct(selectedItem) | |
sender ! Change(newMoneyInside) | |
self ! Done | |
} | |
} | |
class VendingMachineSpec(system: ActorSystem) extends TestKit(system) | |
with ImplicitSender with FunSpecLike | |
with Matchers with BeforeAndAfterAll { | |
import VendingMachine._ | |
describe("一台自動販賣機") { | |
it ("要能接受硬幣而且回傳正確的可選商品列表") { | |
val vendingMachine = system.actorOf(Props[VendingMachine]) | |
vendingMachine ! InsertCoins(10) | |
expectMsg(SelectableProducts(10, Map("生活紅茶"-> Product(10,200)))) | |
vendingMachine ! InsertCoins(15) | |
expectMsg(SelectableProducts(25, Map("生活紅茶" -> Product(10,200), "可口可樂" -> Product(25,100), "無糖綠茶" -> Product(20,200), "雪碧" -> Product(25,100)))) | |
} | |
it ("要可以在使用者按下退幣鈕後退正確的金額給使用者") { | |
val vendingMachine = system.actorOf(Props[VendingMachine]) | |
vendingMachine ! InsertCoins(10) | |
vendingMachine ! InsertCoins(15) | |
vendingMachine ! Refund | |
expectMsgType[SelectableProducts] | |
expectMsgType[SelectableProducts] | |
expectMsg(Change(25)) | |
vendingMachine ! InsertCoins(100) | |
vendingMachine ! Refund | |
expectMsgType[SelectableProducts] | |
expectMsg(Change(100)) | |
} | |
it ("要可以出貨使用者選的商品並找零") { | |
val vendingMachine = system.actorOf(Props[VendingMachine]) | |
vendingMachine ! InsertCoins(50) | |
vendingMachine ! InsertCoins(10) | |
vendingMachine ! InsertCoins(5) | |
expectMsgType[SelectableProducts] | |
expectMsgType[SelectableProducts] | |
expectMsgType[SelectableProducts] | |
vendingMachine ! Selection("可口可樂") | |
expectMsgAllOf(10.second, SendProduct("可口可樂"), Change(40)) | |
} | |
it ("出貨和找零後,販賣機要回到 Idle 狀態,而且投幣金額要為 0 且商品數量被更新") { | |
import akka.testkit.TestFSMRef | |
implicit val actorSystem = system | |
val vendingMachine = TestFSMRef(new VendingMachine) | |
vendingMachine ! InsertCoins(65) | |
vendingMachine ! Selection("可口可樂") | |
vendingMachine.stateName shouldBe Idle | |
vendingMachine.stateData.currentMoneyInside shouldBe 0 | |
vendingMachine.stateData.products.get("可口可樂").map(_.stock) shouldBe Some(99) | |
} | |
} | |
def this() = this(ActorSystem("MySpec")) | |
override def afterAll { | |
TestKit.shutdownActorSystem(system) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment