Menu
CoddyTech

状态模式

本课属于 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
}
challenge icon

挑战

简单

自动售货机处于给定状态之一: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}")
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Kotlin 在线编辑器