状态模式
本课属于 Coddy Kotlin 学习旅程的“面向对象编程”单元。第 49 课,共 57 课。
状态模式会让对象在不同状态下表现出不同的行为。每种状态都是自己的类型,对象会把每个事件交给当前状态,而当前状态会返回下一个状态:
interface LampState {
val name: String
fun press(): LampState
}
object Off : LampState {
override val name = "off"
override fun press() = Dim
}
object Dim : LampState {
override val name = "dim"
override fun press() = Bright
}
object Bright : LampState {
override val name = "bright"
override fun press() = Off
}
class Lamp {
var state: LampState = Off
private set
fun press() { state = state.press() }
}在 main 中:
val lamp = Lamp()
repeat(4) {
lamp.press()
println(lamp.state.name)
}输出:
dim
bright
off
dim状态可以携带数据。使用密封接口时,data object 表示不带数据的状态,数据类表示带数据的状态,而一个函数会为每个事件返回下一个状态:
sealed interface Parking
data object Free : Parking
data class Taken(val plate: String, val since: Int) : Parking
fun Parking.arrive(plate: String, time: Int): Parking = when (this) {
Free -> Taken(plate, time)
is Taken -> this
}
fun Parking.leave(time: Int): Pair<Parking, Int> = when (this) {
Free -> this to 0
is Taken -> Free to (time - since) * 2
}在 main 中:
var spot: Parking = Free
spot = spot.arrive("AB12", 9)
println(spot)
val (next, fee) = spot.leave(12)
println("$next, fee $fee")输出:
Taken(plate=AB12, since=9)
Free, fee 6某些事件在某些状态下不允许发生。对于这些事件,转换可以返回 null,对象随后会停留在原处,并报告拒绝操作:
sealed interface Door
data object Open : Door
data object Closed : Door
data object Locked : Door
fun next(door: Door, event: String): Door? = when (door) {
Open -> if (event == "close") Closed else null
Closed -> when (event) { "open" -> Open; "lock" -> Locked; else -> null }
Locked -> if (event == "unlock") Closed else null
}在 main 中:
var door: Door = Open
for (e in listOf("lock", "close", "lock", "open", "unlock")) {
val n = next(door, e)
if (n == null) println("$e refused while $door") else door = n
}
println("final: $door")输出:
lock refused while Open
open refused while Locked
final: Closed为什么不用几个 Boolean 标志?像 isOpen 和 isLocked 这样的标志可能产生不可能的组合,例如同时处于打开和锁定状态,而且每个方法都必须检查它们。一个状态属性只保存一个有效状态,而对密封类型使用 when 可以让编译器检查是否处理了每种状态:
// 标志位:共 4 种组合,其中一种不可能出现
class DoorWithFlags {
var isOpen = false
var isLocked = false // isOpen && isLocked 不能同时成立
}
// 状态:始终只能是 Open、Closed、Locked 中的一种
class DoorWithState {
var state: Door = Closed
private set
}挑战
简单自动售货机处于给定状态之一:Idle、HasCoins(cents) 或 SoldOut。编写 Vending.label(),使其返回 idle、coins 100 或 sold out,并编写类 VendingMachine,其 state 初始为 Idle,且只能在类内部更改。handle(event) 会应用一个事件并返回 true;如果事件不允许,则返回 false,且不做任何更改:coin <n> 会使状态从 Idle 变为 HasCoins(n),并将金额累加到 HasCoins 中;在 HasCoins 状态下,buy 事件要求至少有 150 分钱,此时会完成一笔销售(计入 sales),并返回 Idle;在 HasCoins 状态下,refund 会使状态变为 Idle;empty 会使任意状态变为 SoldOut;在 SoldOut 状态下,refill 会使状态变为 Idle。
给定的代码会按顺序应用这些事件,并在每个事件后打印状态标签,或者打印 not allowed: <event>,最后打印 sales: 1。
你的代码应写在 VendingMachine.kt、Label.kt、Vending.kt、Idle.kt、HasCoins.kt 和 SoldOut.kt 中。Main.kt 包含给定的输入/输出代码,不能编辑。
亲手试试
fun main() {
// 提供的输入输出代码:保持原样。
val input = generateSequence(::readLine).toList()
val machine = VendingMachine()
for (event in input) {
if (machine.handle(event)) println(machine.state.label()) else println("not allowed: $event")
}
println("sales: ${machine.sales}")
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Kotlin 在线编辑器