告别 Kotlin 中臃肿的 when 表达式

你是否管理过那些根据内部状态发生剧烈行为变化的复杂对象?

如果是,那你很可能已经和一个常见敌人斗争过------遍布你类中每个方法的、冗长的 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(维护)状态,就会破坏开闭原则,因为我们必须修改 insertMoneyselectProduct 以及所有其他动作方法。

经典的状态模式

状态模式通过将"状态"本身变成一个持有行为的对象来解决这个问题。我们希望按状态来组织行为,而不是按动作。

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 语句"之苦,不妨试试状态模式。团队中的小伙伴一定会感谢你的。

相关推荐
阿巴斯甜17 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker18 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952719 小时前
Andorid Google 登录接入文档
android
黄林晴20 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android