状态模式入门:状态模式的定义、核心角色与UML类图

好,咱们今天来聊聊状态模式。说实话,这个模式在 Kotlin 里用起来特别顺手,尤其是配合密封类,简直是天作之合。我最早接触状态模式是在做一个订单系统的重构,那时候代码里全是 if-else 嵌套,改一个状态就要动好几个地方,改得我头皮发麻。后来用了状态模式,整个世界清净了。

状态模式的定义

状态模式,说白了就是:允许一个对象在其内部状态改变时改变它的行为。对象看起来好像修改了它的类。

这句话怎么理解?我举个例子。你想想看,一个电梯门,它有两种状态:打开和关闭。门打开的时候,你按关门按钮,它执行关门动作;门关着的时候,你按开门按钮,它执行开门动作。同一个按钮,在不同状态下,行为完全不同。这就是状态模式的核心思想——把状态和对应的行为封装在一起。

核心定义:状态模式将每个状态封装成一个独立的类,并将与状态相关的行为委托给当前状态对象来处理。这样,当状态发生变化时,对象的行为也随之改变。

我个人习惯把状态模式理解成「状态机」的面向对象实现。每个状态都是一个对象,状态之间的转换由状态对象自己控制。嗯,这里要注意,状态对象通常不包含业务逻辑,它只负责「当前状态下该做什么」以及「下一个状态是什么」。

状态模式的核心角色

状态模式有四个核心角色,我一个个说清楚。

角色 名称 职责
Context 上下文 持有当前状态对象的引用,对外提供统一的接口
State 抽象状态 定义所有具体状态的公共接口
ConcreteState 具体状态 实现抽象状态接口,定义本状态下的具体行为
Client 客户端 通过 Context 与状态交互,不直接操作状态对象

咱们一个个拆开来看。

1. Context(上下文)

Context 是状态模式的门面。它维护一个 State 对象的引用,所有外部请求都通过 Context 转发给当前状态对象。我在项目中遇到过一种情况:有人把业务逻辑直接写在 Context 里,结果 Context 越来越臃肿。记住,Context 只负责状态切换和请求转发,不负责具体实现。

class OrderContext {
    private var state: OrderState = PendingState()

    fun next() {
        state.next(this)
    }

    fun cancel() {
        state.cancel(this)
    }

    fun changeState(newState: OrderState) {
        state = newState
    }
}

2. State(抽象状态)

State 是一个接口或抽象类,定义了所有具体状态必须实现的方法。这些方法对应着 Context 可能接收到的各种请求。说白了,State 就是一份「状态行为清单」。

interface OrderState {
    fun next(context: OrderContext)
    fun cancel(context: OrderContext)
}

3. ConcreteState(具体状态)

每个具体状态实现 State 接口,并定义在该状态下执行某个操作时的具体行为。同时,具体状态还负责决定状态转换的规则——也就是「当前状态下执行某个操作后,应该切换到哪个状态」。

class PendingState : OrderState {
    override fun next(context: OrderContext) {
        println("订单已确认,进入发货状态")
        context.changeState(ShippedState())
    }

    override fun cancel(context: OrderContext) {
        println("订单已取消")
        context.changeState(CancelledState())
    }
}

class ShippedState : OrderState {
    override fun next(context: OrderContext) {
        println("订单已送达,进入完成状态")
        context.changeState(CompletedState())
    }

    override fun cancel(context: OrderContext) {
        println("已发货,无法取消")
    }
}

避坑指南:我曾经在具体状态里直接 new 了下一个状态对象,导致状态之间的耦合度很高。后来我改用工厂方法或者依赖注入来创建状态对象,这样每个状态只关心「要切换到哪个状态」,而不关心「怎么创建那个状态」。

4. Client(客户端)

客户端只和 Context 打交道。它不需要知道当前是什么状态,也不需要知道状态之间怎么转换。客户端只需要调用 Context 提供的统一方法即可。

fun main() {
    val order = OrderContext()
    order.next()   // 订单已确认,进入发货状态
    order.next()   // 订单已送达,进入完成状态
    order.cancel() // 已完成,无法取消
}

状态模式的UML类图

UML 类图这东西,说实话,我一开始也觉得抽象。但画出来之后,结构一目了然。咱们来看状态模式的类图结构。

┌─────────────────┐       ┌──────────────────────┐
│   Context       │       │   «interface»        │
│─────────────────│       │   State              │
│ - state: State  │──────▶│──────────────────────│
│─────────────────│       │ + handle(Context)    │
│ + request()     │       └──────────────────────┘
└─────────────────┘                  ▲
                                     │
                    ┌────────────────┼────────────────┐
                    │                │                │
         ┌──────────┴──────┐  ┌─────┴─────────┐  ┌──┴────────────┐
         │ ConcreteStateA  │  │ ConcreteStateB │  │ ConcreteStateC│
         │─────────────────│  │────────────────│  │───────────────│
         │ + handle(ctx)   │  │ + handle(ctx)  │  │ + handle(ctx) │
         └─────────────────┘  └────────────────┘  └───────────────┘

这个图怎么看?我带你过一遍。

  • Context 持有一个 State 类型的引用(图中用带箭头的实线表示)。
  • State 是一个接口,定义了 handle 方法(图中用虚线空心三角表示实现关系)。
  • ConcreteStateA、B、C 都实现了 State 接口(图中用实线空心三角表示继承/实现)。
  • Context 的 request() 方法内部会调用 state.handle(this),把请求委托给当前状态对象。

你可能会问:为什么 Context 要把自己传给 State?因为 State 在处理完请求后,可能需要调用 Context 的 changeState() 方法来切换状态。这就是所谓的「双向关联」——Context 知道当前状态,状态也知道它属于哪个 Context。

注意:双向关联虽然方便,但也容易造成循环依赖。我建议在 State 中只调用 Context 的 changeState() 方法,不要直接操作 Context 的其他属性。这样能保持职责清晰,也方便后续测试。

状态模式的核心思想总结

说了这么多,咱们来捋一捋状态模式到底解决了什么问题。

  1. 把条件判断变成对象:每个状态是一个对象,不再需要大量的 if-else 或 when 分支。
  2. 状态转换集中管理:每个状态自己决定下一个状态是什么,转换逻辑不会散落在各处。
  3. 开闭原则:要增加一个新状态,只需要新增一个 ConcreteState 类,不需要修改现有代码。
  4. 单一职责:每个状态类只负责自己状态下的行为,职责清晰。

我个人觉得,状态模式最妙的地方在于:它把「状态」和「行为」绑定在了一起。你不需要在 Context 里写一堆 switch-case 来判断当前状态该做什么,因为每个状态对象自己就知道该怎么做。这就像你让不同的人去干不同的事,而不是让一个人记住所有事该怎么做——后者迟早会出乱子。

好,状态模式的定义、角色和类图就讲到这里。下一节咱们会结合 Kotlin 的密封类,看看怎么把状态模式写得更加优雅、安全。密封类加上状态模式,嗯,那才是真正的 Kotlin 风格。