Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

20_状态模式 State Pattern

1. 基本信息

中文名称: 状态模式

英文名称: State Pattern

模式类型: 行为型设计模式

Rust 中常见实现方式: enum 状态机、match 分发、trait + 状态对象、类型状态模式 Typestate

状态模式的核心是:当对象内部状态发生变化时,对象的行为也随之改变,使对象在不同状态下表现出不同的行为。

在 Rust 中,状态模式最常见的写法不是传统 OOP 中的“一个状态一个类”,而是优先使用:

enum 表达有限状态
match 处理不同状态行为
方法控制状态转换

如果需要在编译期禁止非法状态操作,还可以使用 Rust 很有特色的 类型状态模式 Typestate Pattern

代码示例


2. 模式核心思想

状态模式的核心思想是:

把对象的不同行为和它所处的状态关联起来。
当状态变化时,对象的行为也随之变化。

也就是说,状态模式关注的是:

同一个对象,在不同状态下,对同一个操作有不同反应。

例如订单系统中:

未支付订单:可以支付,可以取消
已支付订单:可以发货,不应该重复支付
已发货订单:可以确认收货,不应该取消
已完成订单:不能继续修改

这些行为都和“当前状态”有关。


3. 这个模式解决什么问题

状态模式主要解决的是:对象存在多个状态,并且不同状态下行为不同的问题。

在实际开发中,如果一个对象状态较多,经常会写出大量判断:

if state == "created" { ... }
else if state == "paid" { ... }
else if state == "shipped" { ... }
else if state == "finished" { ... }

这种写法一开始简单,但状态越来越多后会变得难维护。

状态模式试图解决:

1. 状态判断分散在多个地方
2. 状态变化规则不清晰
3. 不同状态下的行为混在一起
4. 新增状态时需要修改大量 if/else 或 match
5. 容易出现非法状态转换

状态模式的目标是:把状态本身和状态对应的行为组织清楚。


4. 不使用这个模式会怎样

如果不使用状态模式,可能出现以下问题:

1. 代码中充满状态判断。
2. 状态转换逻辑分散,难以追踪。
3. 新增状态时,需要修改多个函数。
4. 不同状态下的行为混在一起,代码可读性差。
5. 容易出现非法状态,例如已完成订单又被取消。

在 Rust 中,如果用字符串表示状态,问题会更明显:

"Created"
"created"
"CREATED"
"creatd"

这些字符串编译器无法检查,写错了也可能运行时才发现。

所以 Rust 更推荐用 enum 表达状态。


5. 传统面向对象中的实现思路

在传统 OOP 中,状态模式通常包含三个角色:

1. Context:上下文对象,持有当前状态。
2. State:状态接口,定义状态相关行为。
3. ConcreteState:具体状态类,实现不同行为和状态转换。

典型结构是:

Context
 ├── state: Box<State>
 └── request()

State
 └── handle(context)

ConcreteStateA
 └── handle(context)

ConcreteStateB
 └── handle(context)

调用方调用 Context 的方法,Context 把行为委托给当前 State 对象。不同状态对象有不同实现。

这种写法的好处是:每个状态类封装自己的行为。

但缺点也明显:

1. 状态类数量可能很多。
2. 状态转换逻辑可能分散到多个状态类里。
3. 类和对象结构较复杂。
4. 简单状态机使用这种写法会显得过度设计。

6. Rust 中的实现思路

6.1 Rust 中通常怎么实现

Rust 中最常见、最自然的状态模式实现方式是:

enum + match

例如订单状态可以写成:

enum OrderState {
    Created,
    Paid,
    Shipped,
    Finished,
    Cancelled,
}

struct Order {
    state: OrderState,
}

impl Order {
    fn new() -> Self {
        Self {
            state: OrderState::Created,
        }
    }

    fn pay(&mut self) {
        match self.state {
            OrderState::Created => {
                println!("订单支付成功");
                self.state = OrderState::Paid;
            }
            _ => {
                println!("当前状态不能支付");
            }
        }
    }

    fn ship(&mut self) {
        match self.state {
            OrderState::Paid => {
                println!("订单已发货");
                self.state = OrderState::Shipped;
            }
            _ => {
                println!("当前状态不能发货");
            }
        }
    }

    fn finish(&mut self) {
        match self.state {
            OrderState::Shipped => {
                println!("订单已完成");
                self.state = OrderState::Finished;
            }
            _ => {
                println!("当前状态不能完成");
            }
        }
    }
}

fn main() {
    let mut order = Order::new();

    order.pay();
    order.ship();
    order.finish();
}

这种写法的特点是:

1. 状态是有限且明确的。
2. match 会强制处理状态分支。
3. 状态转换集中在对象方法中。
4. 不需要为每个状态创建独立结构体。

6.2 和传统 OOP 写法相比有什么不同

和传统面向对象语言相比,Rust 的状态模式有几个明显特点。

第一,Rust 更倾向于用 enum 表达有限状态。 传统 OOP 中可能会为每个状态创建一个类,而 Rust 中很多状态本质上是一组有限取值,用 enum 更直接。

第二,Rust 的 match 可以检查状态是否处理完整。 如果状态枚举新增了一个变体,相关 match 可能会提示你需要补充分支。这比字符串状态或状态码更安全。

第三,Rust 可以让不同状态携带不同数据。 例如:

#![allow(unused)]
fn main() {
enum DownloadState {
    Waiting,
    Downloading { progress: u8 },
    Finished { path: String },
    Failed { reason: String },
}
}

这比传统写法中把所有字段都放在一个对象里更清晰。

第四,Rust 可以通过类型系统限制非法操作。 如果只用 enum + match,非法操作通常是在运行时判断;如果使用类型状态模式,可以在编译期禁止某些操作。

第五,Rust 不依赖继承体系。 状态行为可以通过 enum + match、trait、泛型或类型状态实现,而不是必须依赖“状态父类 + 具体状态子类”。


6.3 Rust 中是否有更自然的替代写法

在 Rust 中,状态模式主要有三种实现层次。

第一种是 enum 状态机。 适合状态数量有限、状态转换规则清晰的场景。这是最推荐的入门写法。

第二种是 trait + 状态对象。 适合状态行为非常复杂,并且不同状态之间差异很大时。写法更接近传统 OOP。

第三种是 类型状态模式 Typestate。 适合希望在编译期限制非法操作的场景。

例如网络连接:

未连接状态:只能 connect
已连接状态:才能 send
已关闭状态:不能 send

如果使用类型状态模式,可以让“未连接时调用 send”直接编译失败,而不是运行时报错。


7. Rust 中涉及的语言特性

状态模式在 Rust 中主要涉及以下语言特性:

1. enum
2. match
3. struct
4. impl
5. trait
6. 泛型
7. 所有权移动
8. 类型状态模式 Typestate
9. Option / Result

这些特性分别解决的问题是:

enum:表达有限状态
match:根据状态分发行为
struct:保存对象数据
impl:定义状态转换方法
trait:抽象复杂状态行为
泛型:表达不同类型状态
所有权移动:表达状态转换后旧状态失效
Option / Result:表达状态转换成功或失败

其中最核心的是:

enum + match

如果要进一步体现 Rust 特色,则要理解:

Typestate 类型状态模式

8. 性能与工程代价

状态模式的性能和具体实现方式有关。

如果使用 enum + match

1. 性能通常很好。
2. 状态分支在编译期明确。
3. 没有堆分配。
4. 没有动态分发。
5. 适合绝大多数普通状态机。

如果使用 Box<dyn State> 这类 trait object:

1. 会有动态分发开销。
2. 可能涉及堆分配。
3. 结构更灵活,但性能和复杂度成本更高。

如果使用类型状态模式:

1. 很多错误可以在编译期发现。
2. 运行时状态判断可能减少。
3. 代码类型数量会增加。
4. 初学者理解成本较高。

所以在 Rust 中选择状态模式实现方式时,可以遵循:

简单状态机:enum + match
复杂行为状态:trait + 状态对象
强约束流程:Typestate 类型状态模式

一句话总结:

Rust 中状态模式的首选通常是 enum + match,性能好、表达清晰、类型安全。

9. 典型应用场景

状态模式适合用于对象行为明显依赖状态的场景。

常见应用包括:

1. 订单流程:创建、支付、发货、完成、取消
2. 网络连接:未连接、已连接、断开、重连中
3. 下载任务:等待、下载中、完成、失败
4. 播放器:停止、播放中、暂停
5. 文档审核:草稿、审核中、已发布、已驳回
6. 游戏角色:普通、攻击、防御、眩晕、死亡
7. 编译流程:词法分析、语法分析、类型检查、代码生成
8. 设备控制:未初始化、已初始化、运行中、故障

在 Rust 项目中,状态模式经常出现在:

协议解析
任务调度
异步流程控制
文件处理流程
网络客户端
编译器或解释器
嵌入式设备状态管理

10. 和相似模式的区别

状态模式容易和策略模式、模板方法模式、责任链模式、备忘录模式混淆。

10.1 状态模式和策略模式

二者都可能表现为“不同行为可以切换”,但关注点不同。

策略模式关注:算法或行为可以被调用方主动替换。
状态模式关注:对象内部状态变化后,行为自动变化。

策略模式更像是:

我选择用哪种算法。

状态模式更像是:

对象处于什么状态,就表现出什么行为。

例如:

选择 zip/gzip 压缩算法:策略模式
订单从未支付变成已支付后行为变化:状态模式

10.2 状态模式和模板方法模式

模板方法模式关注的是固定流程。

状态模式关注的是不同状态下行为不同。

模板方法模式:流程骨架固定,部分步骤可变。
状态模式:对象状态变化后,同一操作的行为变化。

例如:

文件处理固定为读取 → 解析 → 保存:模板方法模式
播放器在播放、暂停、停止状态下点击按钮行为不同:状态模式

10.3 状态模式和责任链模式

责任链模式关注请求沿链传递。

状态模式关注对象自身状态变化。

责任链模式:请求交给谁处理。
状态模式:当前状态决定如何处理。

例如:

Web 请求经过多个中间件:责任链模式
订单根据当前状态决定是否能取消:状态模式

10.4 状态模式和备忘录模式

二者都涉及状态,但目的不同。

状态模式:根据当前状态改变行为。
备忘录模式:保存历史状态,方便之后恢复。

例如:

编辑器处于编辑/只读/预览状态:状态模式
保存编辑器历史内容用于撤销:备忘录模式

11. 使用该模式的优点

状态模式的优点主要有:

1. 将状态和行为关联起来,逻辑更清晰。
2. 减少大量散落的 if/else 状态判断。
3. 状态转换规则更容易集中管理。
4. 新增状态时结构更清楚。
5. Rust 中使用 enum 可以获得编译期检查。
6. 使用类型状态模式可以在编译期禁止非法操作。

从 Rust 角度看,状态模式最大的优点是:

用类型系统表达状态边界,让错误尽量提前暴露。

12. 使用该模式的代价

状态模式的代价主要有:

1. 状态数量多时,match 分支可能变长。
2. 如果用 trait 状态对象,结构会更复杂。
3. 如果用 Typestate,类型数量会增加。
4. 状态转换分散不当时,仍然会难以维护。
5. 对简单状态使用复杂状态模式会显得过度设计。

特别是在 Rust 中,不能一看到状态就立刻写 Box<dyn State>

很多时候:

enum + match

已经足够清晰。


13. 什么时候不应该使用

以下情况不适合使用复杂的状态模式:

1. 状态只有一两个,而且逻辑很简单。
2. 状态不会影响对象行为。
3. 普通布尔字段已经能清楚表达。
4. 只是临时判断,不需要长期维护。
5. 为了模仿传统 OOP,强行把每个状态拆成对象。

在 Rust 中可以遵循一个简单原则:

状态少、逻辑简单:普通字段或 enum 即可。
状态多、行为明显不同:状态模式。
非法操作必须编译期禁止:Typestate。

14. 一个简单例子思路

场景: 订单状态流转

订单初始状态是 Created。
Created 状态可以支付,也可以取消。
Paid 状态可以发货。
Shipped 状态可以确认完成。
Finished 状态不能继续修改。
Cancelled 状态不能继续操作。

处理流程可以设计为:

1. 使用 enum OrderState 表达状态。
2. Order 结构体保存当前状态。
3. pay、ship、finish、cancel 方法根据当前状态执行不同逻辑。
4. 非法状态转换返回错误或提示。
5. 后续可以升级为 Typestate,让非法操作在编译期报错。

适合练习的 Rust 特性:

enum
match
impl
Result
所有权移动
Typestate

15. 总结一句话

状态模式的本质是:

把对象行为和对象当前状态绑定起来,使对象在不同状态下对同一操作表现出不同的行为。

在 Rust 中,状态模式最自然的实现方式通常是 enum + match。当状态转换规则复杂时,可以结合 trait 状态对象;当希望编译期禁止非法操作时,可以进一步使用类型状态模式 Typestate。