
你是否管理过那些根据内部状态发生剧烈行为变化的复杂对象?
如果是,那你很可能已经和一个常见敌人斗争过------遍布你类中每个方法的、冗长的 when 语句。
它看起来大概是这样的:
kotlin
fun handleAction() {
when (state) {
State.A -> { /* A 的逻辑 */ }
State.B -> { /* B 的逻辑 */ }
State.C -> { /* C 的逻辑 */ }
// ... 还在不断增长!
}
}
每次你添加一个新状态(比如 State.D),你就得逐个追踪并更新每一个函数,加入新的分支。
这是个维护噩梦,直接违反了开闭原则(OCP),也是一种严重的代码异味。你的逻辑本该按状态分组,却因为动作而被散乱地分布在各处。
那么,解决方案是什么呢?
状态模式。而且借助现代 Kotlin,我们可以用最少的样板代码,实现最优雅的状态模式。
一个简单的自动售货机

让我们想象一下,我们正在构建一个简单的 VendingMachine。它有几个关键的状态和动作:
- 状态 :
IDLE(空闲)、HAS_MONEY(已投币)、OUT_OF_STOCK(缺货) - 动作 :
insertMoney(投币)、selectProduct(选择商品)、requestRefund(请求退款)
下面是那种存在问题的、基于状态的检查代码:
kotlin
class VendingMachine(private var balance: Int = 0) {
private var state: State = State.IDLE
fun insertMoney(amount: Int) {
when (state) {
State.IDLE -> {
balance += amount
state = State.HAS_MONEY
println("已投币。余额:$balance")
}
State.HAS_MONEY -> {
println("已投币,无需重复投币。余额:$balance")
}
State.OUT_OF_STOCK -> {
println("机器缺货。")
}
}
}
fun selectProduct(code: String) {
when (state) {
State.IDLE -> {
println("请先投币。")
}
State.HAS_MONEY -> {
if (/* 商品可用 */) {
println("商品已售出。")
state = State.IDLE
} else {
println("商品不可用。")
}
}
State.OUT_OF_STOCK -> {
println("机器缺货。")
}
}
}
fun requestRefund() {
when (state) {
State.IDLE -> {
println("没有可退的款项。")
}
State.HAS_MONEY -> {
println("已退款:$balance")
balance = 0
state = State.IDLE
}
State.OUT_OF_STOCK -> {
println("没有可退的款项。")
}
}
}
}
如果我们新增一个 MAINTENANCE(维护)状态,就会破坏开闭原则,因为我们必须修改 insertMoney、selectProduct 以及所有其他动作方法。
经典的状态模式
状态模式通过将"状态"本身变成一个持有行为的对象来解决这个问题。我们希望按状态来组织行为,而不是按动作。
1. 定义接口
我们定义一个通用的接口(MachineState),所有状态对象都必须实现它。这个接口包含了上下文(VendingMachineContext)可以执行的所有可能动作。
kotlin
interface MachineState {
fun insertMoney(amount: Int, context: VendingMachineContext)
fun selectProduct(code: String, context: VendingMachineContext)
fun requestRefund(context: VendingMachineContext)
}
2. 具体实现
我们为每个状态实现其行为。关键点在于:单个状态的所有逻辑现在都集中在一个类/对象中。
kotlin
object IdleState : MachineState {
override fun insertMoney(amount: Int, context: VendingMachineContext) {
context.balance += amount
context.state = HasMoneyState
println("已投币。余额:${context.balance}")
}
override fun selectProduct(code: String, context: VendingMachineContext) {
println("请先投币。")
}
override fun requestRefund(context: VendingMachineContext) {
println("没有可退的款项。")
}
}
object HasMoneyState : MachineState {
override fun insertMoney(amount: Int, context: VendingMachineContext) {
context.balance += amount
println("已添加金额。余额:${context.balance}")
}
override fun selectProduct(code: String, context: VendingMachineContext) {
if (context.balance >= 2) {
context.balance -= 2
println("商品已售出。")
context.state = IdleState
} else {
println("商品不可用。")
}
}
override fun requestRefund(context: VendingMachineContext) {
println("已退款:${context.balance}")
context.balance = 0
context.state = IdleState
}
}
object OutOfStockState : MachineState {
override fun insertMoney(amount: Int, context: VendingMachineContext) {
println("机器缺货。")
}
override fun selectProduct(code: String, context: VendingMachineContext) {
println("机器缺货。")
}
override fun requestRefund(context: VendingMachineContext) {
println("没有可退的款项。")
}
}
3. 上下文
VendingMachineContext(即上下文)现在变得简洁,只需将动作委托给其当前的状态对象即可。
kotlin
class VendingMachineContext(var balance: Int = 0) {
var state: MachineState = IdleState
fun insertMoney(amount: Int) {
state.insertMoney(amount, this)
}
fun selectProduct(code: String) {
state.selectProduct(code, this)
}
fun requestRefund() {
state.requestRefund(this)
}
// 辅助函数,供状态对象切换机器状态
fun transitionTo(newState: MachineState) {
this.state = newState
}
}
如果你新增一个状态,只需创建一个新的类。你无需修改 VendingMachineContext 或其他任何状态类。开闭原则得到了恢复!
增强的 Kotlin 风格状态模式
经典模式很可靠,但 Kotlin 为我们提供了更强大、更优雅的方式来构建它:使用 密封接口 来强制实现类型安全的命令。
1. 定义所有输入
我们使用密封接口来建模机器可以接收的所有可能输入:
kotlin
sealed interface VendingInput {
data class InsertMoney(val amount: Int) : VendingInput
data class SelectProduct(val code: String) : VendingInput
object RequestRefund : VendingInput
}
2. 定义状态和上下文
状态接口现在接受一个单一的 VendingInput。上下文拥有一个公开的 process 方法。
kotlin
sealed interface VendingState {
fun handle(input: VendingInput, context: VendingMachineContext)
}
class VendingMachineContext(var balance: Int = 0) {
var state: VendingState = IdleState
fun process(input: VendingInput) {
state.handle(input, this)
}
fun transitionTo(newState: VendingState) {
this.state = newState
}
}
3. 行为
状态对象使用一个对 input 类型的单一 when 语句。因为 VendingInput 是密封的,Kotlin 会强制我们处理所有可能的输入------这使得我们的代码变得穷举且安全!
kotlin
object IdleState : VendingState {
override fun handle(input: VendingInput, context: VendingMachineContext) {
when (input) {
is VendingInput.InsertMoney -> {
context.balance += input.amount
context.transitionTo(HasMoneyState)
println("已投币。余额:${context.balance}")
}
is VendingInput.SelectProduct -> {
println("请先投币。")
}
VendingInput.RequestRefund -> {
println("没有可退的款项。")
}
}
}
}
object HasMoneyState : VendingState {
override fun handle(input: VendingInput, context: VendingMachineContext) {
when (input) {
is VendingInput.InsertMoney -> {
context.balance += input.amount
println("已添加金额。余额:${context.balance}")
}
is VendingInput.SelectProduct -> {
if (context.balance >= 2) {
context.balance -= 2
println("商品已售出。")
context.transitionTo(IdleState)
} else {
println("商品不可用。")
}
}
VendingInput.RequestRefund -> {
println("已退款:${context.balance}")
context.balance = 0
context.transitionTo(IdleState)
}
}
}
}
object OutOfStockState : VendingState {
override fun handle(input: VendingInput, context: VendingMachineContext) {
when (input) {
is VendingInput.InsertMoney -> {
println("机器缺货。")
}
is VendingInput.SelectProduct -> {
println("机器缺货。")
}
VendingInput.RequestRefund -> {
println("没有可退的款项。")
}
}
}
}
纯函数式状态转换
对于真正健壮的系统(如 MVI),你可能希望避免可变性。你可以通过让状态函数返回下一个状态来实现这一点,从而使系统具有高度确定性:
1. 定义转换
kotlin
// 1. 定义所有输入(保持不变,密封接口非常适合函数式)
sealed interface VendingInput {
data class InsertMoney(val amount: Int) : VendingInput
data class SelectProduct(val code: String) : VendingInput
object RequestRefund : VendingInput
}
// 2. 定义状态:将数据(balance)与状态逻辑结合
// 使用不可变属性 (val),确保状态一旦创建不可修改
sealed class VendingState(val balance: Int) {
// 核心转变:函数签名返回 (新状态, 产生的结果文本)
abstract fun handle(input: VendingInput): Pair<VendingState, String>
// --- 各个状态的具体实现 ---
data class Idle(private val b: Int = 0) : VendingState(b) {
override fun handle(input: VendingInput) = when (input) {
is VendingInput.InsertMoney ->
HasMoney(balance + input.amount) to "已投币。余额:${balance + input.amount}"
is VendingInput.SelectProduct ->
this to "请先投币。"
VendingInput.RequestRefund ->
this to "没有可退的款项。"
}
}
data class HasMoney(private val b: Int) : VendingState(b) {
override fun handle(input: VendingInput) = when (input) {
is VendingInput.InsertMoney ->
copy(b = balance + input.amount) to "已添加金额。余额:${balance + input.amount}"
is VendingInput.SelectProduct ->
if (balance >= 2) Idle(balance - 2) to "商品已售出:${input.code}。"
else this to "余额不足。"
VendingInput.RequestRefund ->
Idle(0) to "已退款:$balance"
}
}
object OutOfStock : VendingState(0) {
override fun handle(input: VendingInput) = this to "机器缺货。"
}
}
2. 使用举例
kotlin
fun main() {
val s0 = VendingState.Idle()
// 所有的处理结果都作为新值返回,原始 s0 保持不变
val (s1, msg1) = s0.handle(VendingInput.InsertMoney(5))
println(msg1) // 已投币。余额:5
val (s2, msg2) = s1.handle(VendingInput.SelectProduct("可乐"))
println(msg2) // 商品已售出:可乐。
// s2 现在是 Idle 状态,余额已扣除
println("最终状态: ${s2::class.simpleName}, 余额: ${s2.balance}")
}
虽然更复杂,但这种方法在响应式架构中很常见,因为状态变化是必须被精确跟踪的事件。
它的核心实际上就是函数有状态的返回,而状态本身是不可变的,如果同一个状态的数据发生变化,会生成一个新的状态。
总结
通过应用状态模式,我们成功地将分散、脆弱的条件逻辑替换为干净、内聚的状态对象。这使得我们的代码更易于维护、测试和扩展。
如果你的 Kotlin 代码库正遭受"臃肿的 when 语句"之苦,不妨试试状态模式。团队中的小伙伴一定会感谢你的。