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

相关推荐
Whisper_Sy1 天前
Flutter for OpenHarmony移动数据使用监管助手App实战 - 应用列表实现
android·开发语言·javascript·flutter·php
北海屿鹿1 天前
【MySQL】内置函数
android·数据库·mysql
臻一1 天前
rk3576+安卓14 ---上电时序调整
android
踢球的打工仔1 天前
typescript-接口的基本使用(一)
android·javascript·typescript
2501_915918411 天前
如何在iPad上找到并打开文件夹的完整指南
android·ios·小程序·uni-app·iphone·webview·ipad
臻一1 天前
rk3576+安卓14---uboot
android
2501_944521591 天前
Flutter for OpenHarmony 微动漫App实战:主题配置实现
android·开发语言·前端·javascript·flutter·ecmascript
2501_944521591 天前
Flutter for OpenHarmony 微动漫App实战:动漫卡片组件实现
android·开发语言·javascript·flutter·ecmascript
知1而N1 天前
电脑上运行APK文件(Android应用程序包),需要借助特定的软件或功能,因为Windows/macOS/Linux系统无法原生直接运行安卓应用
android·macos·电脑
代码s贝多芬的音符1 天前
HttpURLConnection post多个参数和一个图片
android·httpurlconn