# C++设计模式:状态模式——让对象在不同状态下表现得像“变了一个人” > **引言**:当一个对象的行为开始“看状态下菜” 很多团队第一次真正需要状态模式,不是在 UML 图上画状态机,而是在维护一个越来越长、越来越危险的 `switch(state)`。 假设你在做一个订单系统。订单会经历这些状态: - • 待支付 - • 已支付 - • 已发货 - • 已完成 - • 已取消 最直接的写法通常是这样的: ```cpp #include #include class Order { public: enum classStatus { PendingPayment, Paid, Shipped, Completed, Cancelled }; void pay() { if (status_ == Status::PendingPayment) { std::cout << "支付成功" << std::endl; status_ = Status::Paid; } else { std::cout << "当前状态不允许支付" << std::endl; } } void ship() { if (status_ == Status::Paid) { std::cout << "订单已发货" << std::endl; status_ = Status::Shipped; } else { std::cout << "当前状态不允许发货" << std::endl; } } void complete() { if (status_ == Status::Shipped) { std::cout << "订单已完成" << std::endl; status_ = Status::Completed; } else { std::cout << "当前状态不允许完成" << std::endl; } } void cancel() { if (status_ == Status::PendingPayment || status_ == Status::Paid) { std::cout << "订单已取消" << std::endl; status_ = Status::Cancelled; } else { std::cout << "当前状态不允许取消" << std::endl; } } private: Status status_ = Status::PendingPayment; }; ``` 刚开始看起来似乎没什么问题,但项目一旦继续长大,问题就会越来越明显。 **问题 1:状态判断散落在每个动作里** ``` if (status_ == Status::PendingPayment) { ... } if (status_ == Status::Paid) { ... } if (status_ == Status::Shipped) { ... } ``` 每个行为都要自己判断“当前能不能做”。当状态越来越多、动作越来越多时,判断逻辑会迅速膨胀。 **问题 2:状态迁移规则难以维护** 比如现在又加了“退款中”“退款完成”“部分发货”“售后中”这些状态,原有的: - • `pay()` - • `ship()` - • `complete()` - • `cancel()` 都要跟着改。**状态迁移规则和业务动作强耦合在一起了。** **问题 3:一个对象的行为会随着状态变化而变化** 订单最麻烦的地方不只是“有状态”,而是: - • 同样是调用 `cancel()` - • 在 `PendingPayment` 下允许取消 - • 在 `Shipped` 下就不允许取消 这说明问题已经不再是“保存一个状态字段”那么简单,而是:**对象在不同状态下,行为真的不一样。** **问题 4:`switch(state)` 很快变成维护灾难** ``` switch (status_) { case Status::PendingPayment: // 一套规则 break; case Status::Paid: // 另一套规则 break; case Status::Shipped: // 再来一套规则 break; // 后面还有更多状态... } ``` 这种写法不是不能用,但它的趋势非常明确:**每增加一个状态,多个方法都要一起膨胀。** 生活里也有类似场景: - • **自动售货机**在“待投币”“已投币”“出货中”“故障中”这几个状态下,按同一个按钮,表现完全不同。 - • **游戏角色**在“站立”“奔跑”“跳跃”“受伤”“死亡”状态下,收到同样的输入,也会做出不同反应。 - • **工单系统**在“待处理”“处理中”“待确认”“已关闭”状态下,同样一个“关闭工单”按钮,也不是总能点。 没有状态模式时,通常是这样的: ``` [Context] ├── 保存 state 字段 ├── 每个动作都 switch(state) └── 每个分支里再写状态迁移 ``` 用了状态模式之后,会变成: ``` [Context] ──持有──> [State 接口] ├── 待支付状态 ├── 已支付状态 ├── 已发货状态 └── 已取消状态 ``` **状态模式就是为解决“对象的行为依赖于内部状态,而且状态切换逻辑越来越复杂”这个问题而生的。** ------ ## 状态模式详解 ### 模式定义 > **状态模式(State Pattern)**,核心思想是:**允许一个对象在其内部状态改变时改变它的行为,看起来就像改变了它的类一样。** 这句话第一次读通常有点抽象。更直白一点说: **把“不同状态下的不同行为”拆到不同状态对象里去,让上下文不再到处写 `if(state)` 或 `switch(state)`。** 状态模式最重要的一点不是“有一个状态字段”,而是: - • **状态影响行为** - • **状态还能触发迁移** 所以它本质上是在处理一种“带规则的状态机”。 ### 核心角色 状态模式通常包含三个核心角色: 1. **Context(上下文)**:对外暴露业务接口,内部持有当前状态对象 2. **State(状态接口)**:定义各个状态下共享的行为入口 3. **ConcreteState(具体状态)**:实现某个状态下的行为,并在需要时触发状态切换 可以把它理解成这样: - • `Context` 决定“我是谁、现在处于什么状态” - • `State` 规定“所有状态都必须能处理哪些动作” - • `ConcreteState` 决定“在当前状态下,这个动作该怎么表现” 如果类比生活: - • `Context` 像一台自动售货机 - • `State` 像“售货机当前运行规则”的抽象 - • `ConcreteState` 就是“待投币”“已投币”“出货中”“故障中”这些具体状态 关键点在于:**调用者面对的永远是同一台售货机,但售货机的行为会随着状态切换而变化。** ### 基本实现(经典版本) 先看一个最经典的“音乐播放器状态切换”例子。 我们让播放器支持三个动作: - • `play()` - • `pause()` - • `stop()` 不同状态下,这三个动作的表现并不一样。 ```cpp #include #include #include class PlayerState; class MusicPlayer { public: MusicPlayer(); void setState(std::unique_ptr state); void play(); void pause(); void stop(); void showState(const std::string& name) const { std::cout << "当前状态 -> " << name << std::endl; } private: std::unique_ptr state_; }; class PlayerState { public: virtual ~PlayerState() = default; virtual void play(MusicPlayer& player) = 0; virtual void pause(MusicPlayer& player) = 0; virtual void stop(MusicPlayer& player) = 0; virtual const char* name() const = 0; }; class PlayingState : public PlayerState { public: void play(MusicPlayer& player) override { std::cout << "已经在播放中,无需重复播放" << std::endl; player.showState(name()); } void pause(MusicPlayer& player) override; void stop(MusicPlayer& player) override; const char* name() const override { return"Playing"; } }; class PausedState : public PlayerState { public: void play(MusicPlayer& player) override; void pause(MusicPlayer& player) override { std::cout << "已经暂停,无需重复暂停" << std::endl; player.showState(name()); } void stop(MusicPlayer& player) override; const char* name() const override { return"Paused"; } }; class StoppedState : public PlayerState { public: void play(MusicPlayer& player) override; void pause(MusicPlayer& player) override { std::cout << "停止状态下不能暂停" << std::endl; player.showState(name()); } void stop(MusicPlayer& player) override { std::cout << "已经是停止状态" << std::endl; player.showState(name()); } const char* name() const override { return"Stopped"; } }; MusicPlayer::MusicPlayer() : state_(std::make_unique()) {} void MusicPlayer::setState(std::unique_ptr state) { state_ = std::move(state); } void MusicPlayer::play() { state_->play(*this); } void MusicPlayer::pause() { state_->pause(*this); } void MusicPlayer::stop() { state_->stop(*this); } void PlayingState::pause(MusicPlayer& player) { std::cout << "暂停播放" << std::endl; player.setState(std::make_unique()); player.showState("Paused"); } void PlayingState::stop(MusicPlayer& player) { std::cout << "停止播放" << std::endl; player.setState(std::make_unique()); player.showState("Stopped"); } void PausedState::play(MusicPlayer& player) { std::cout << "继续播放" << std::endl; player.setState(std::make_unique()); player.showState("Playing"); } void PausedState::stop(MusicPlayer& player) { std::cout << "从暂停状态停止播放" << std::endl; player.setState(std::make_unique()); player.showState("Stopped"); } void StoppedState::play(MusicPlayer& player) { std::cout << "开始播放音乐" << std::endl; player.setState(std::make_unique()); player.showState("Playing"); } ``` 这个实现里最关键的不是“有几个类”,而是这两个变化: 1. `MusicPlayer` 不再自己判断 `if (state == xxx)` 2. 各状态自己决定当前动作是否合法,以及切换到哪个状态 这就是状态模式和普通“状态字段 + 条件分支”之间最本质的区别。 ### 经典版本的问题 经典状态模式已经比 `switch(state)` 清晰很多,但它也不是没有代价。 **代价 1:状态类数量会增多** 如果状态很多,你会拥有一批状态类。对于复杂状态机这很合理,但对特别简单的场景可能显得偏重。 **代价 2:状态迁移可能分散** 如果每个状态类都能切换到别的状态,迁移规则会分布在多个类里。 这带来灵活性,也意味着你需要更有意识地管理“状态图”。 **代价 3:状态对象是“纯状态”还是“带数据状态”** 有些状态只是行为差异,不需要额外数据;有些状态则携带计时器、重试次数、上下文信息。 这会直接影响你是共享状态实例,还是每次新建状态对象。 所以在进入实战前,我们先看一个关键技术专题。 ------ ## 关键技术专题:状态迁移放在哪里 状态模式真正容易写乱的地方,不是状态类本身,而是:**迁移规则放在哪里。** ### 方案一:由状态对象主动切换 这就是经典写法: ``` void PlayingState::pause(MusicPlayer& player) { player.setState(std::make_unique()); } ``` 优点: - • 当前状态最清楚“下一步可以去哪” - • 行为和迁移写在一起,读起来顺 缺点: - • 状态之间会彼此知道对方 - • 复杂状态图下,迁移关系容易散落 ### 方案二:由 Context 统一管理迁移 另一种做法是让状态只返回“迁移意图”,真正切换由 `Context` 统一处理。 ``` enum class NextState { Keep, ToPlaying, ToPaused, ToStopped }; ``` 优点: - • 状态图集中管理,更容易审查 - • 更适合流程很严谨的业务系统 缺点: - • 状态行为和状态迁移被拆开 - • 某些场景会显得啰嗦 ### 实际建议 可以这么选: - • **交互型对象**:如播放器、角色控制、UI 组件,优先用“状态对象主动切换” - • **流程型业务**:如订单、审批、工单,优先考虑让 `Context` 统一约束迁移 换句话说: > **状态模式的重点不是“谁调用 `setState()`”,而是要让迁移规则可读、可控、可验证。** ------ ## 现代 C++ 实现 ### 用智能指针管理状态对象 在现代 C++ 中,状态模式最常见的实现方式就是: - • `Context` 持有 `std::unique_ptr` - • 状态切换时使用 `std::make_unique()` 这有几个直接好处: - • 生命周期清晰 - • 不需要手写 `delete` - • 状态切换时资源释放自然发生 例如: ``` class Context { public: void setState(std::unique_ptr state) { state_ = std::move(state); } private: std::unique_ptr state_; }; ``` ### 何时共享状态对象 如果状态对象本身**不包含成员数据**,只是一组行为规则,那么其实可以把它们做成共享单例,避免重复创建。 但我更建议在文章和教学代码里优先使用 `std::unique_ptr` 版本,因为它更直观,也更容易扩展到“状态对象带数据”的情况。 只有当你明确满足这两个条件时,再考虑共享状态: 1. 状态对象完全无内部状态 2. 你非常确定不会因为共享而引入线程安全或重入问题 ### 现代 C++ 下的边界 并不是所有状态机都一定要上完整的状态模式。 如果状态极少、规则极稳定,直接 `enum + switch` 可能已经足够。 状态模式最有价值的场景是: - • 状态很多 - • 每个状态下行为差异明显 - • 迁移规则经常变 - • 你希望每个状态能被单独测试 ------ ## 状态模式与设计原则 ### 开闭原则(Open-Closed Principle) 如果未来订单系统再加一个“退款中”状态,你可以新增一个 `RefundingState`,并调整相关迁移点,而不需要把所有逻辑重新塞回 `OrderContext` 里。 状态模式不能保证你“零修改”,但它能把修改范围更自然地限制在相关状态附近,而不是让一个大类全身都是分支。 ### 单一职责原则(Single Responsibility Principle) 状态模式天然鼓励职责分离: - • `Context` 负责对外提供稳定接口 - • 每个 `ConcreteState` 只关心自己这一个状态下的规则 这样一来,“待支付怎么处理”和“已发货怎么处理”不再混在同一个超大类里。 ### 依赖倒置原则(Dependency Inversion Principle) `Context` 持有的是抽象状态接口: ``` std::unique_ptr state_; ``` 它依赖的是 `OrderState`,而不是具体的 `PendingPaymentState`、`PaidState`、`ShippedState` 实现细节。 这让状态替换与测试都更自然。 ------ ## 常见陷阱与最佳实践 ### 陷阱 1:把简单状态字段硬上状态模式 如果你的类只有两个状态,而且规则几乎不会扩展: ``` bool enabled_; ``` 这时上完整状态模式大概率是过度设计。 状态模式适合的是“状态多、行为差异大、迁移复杂”的场景。 **解决**:先看复杂度趋势,不要为了模式而模式。 ### 陷阱 2:迁移关系散到到处都是 如果每个状态都在无序地跳转到其他所有状态,很快你自己也会迷路。 **解决**: - • 先画清楚状态图 - • 统一梳理允许迁移路径 - • 对非法迁移保持明确输出或错误处理 ### 陷阱 3:把状态对象和上下文数据缠得太紧 如果状态对象为了完成行为,频繁直接操作 `Context` 的大量内部细节,说明封装边界可能出了问题。 **解决**:给 `Context` 暴露少量明确的方法,而不是让状态对象碰所有内部成员。 ### 陷阱 4:终态没有处理好 像“已完成”“已取消”“死亡”这类终态,经常被忘记处理,结果后面还能继续执行动作。 **解决**:把终态当成正式状态对象,明确规定它允许和禁止的行为。 ------ ## 何时使用状态模式 ### 适用场景 - • 一个对象有多个明确状态,而且每个状态下行为明显不同 - • 你发现很多方法里都在重复判断同一个状态字段 - • 状态切换规则本身就是业务核心,需要被清晰表达 - • 你希望每个状态逻辑能被单独测试和演进 - • 你想把庞大的 `switch(state)` 拆成更清晰的结构 ### 不适用场景 - • 只有极少数简单状态,逻辑非常稳定 - • 状态不影响行为,只是一个普通标记字段 - • 迁移规则几乎不存在,或者只有一两个简单分支 - • 引入状态类会比原逻辑更绕,得不偿失 可以用一个简单标准判断: ``` 状态影响行为 + 迁移规则重要 + 分支开始膨胀 -> 推荐用状态模式 状态只是标记 + 规则简单稳定 -> 不必强上 ``` ------ ## 写在最后 状态模式在 C++ 工程里特别有价值,因为很多系统看起来是在“处理动作”,本质上其实是在“处理状态驱动下的动作”。 订单、工作流、角色控制、设备控制、连接管理、UI 组件,这些问题的共同点都不是“功能很多”,而是:**同一个入口,在不同状态下会有不同结果。** 当你开始在多个方法里反复写 `if (state == ...)`、`switch(state)` 时,往往就是一个强烈信号:代码已经在提醒你,应该把状态规则单独建模了。 在现代 C++ 中,状态模式通常和 `std::unique_ptr` 一起使用,既能保持结构清晰,也能让状态切换的生命周期管理足够自然。 当然,它也不是银弹。如果只是两个简单状态,直接分支更朴素;但当状态机变成系统核心逻辑时,状态模式几乎总能把代码从“分支泥潭”里拉出来。 实现演进路线: ``` 状态字段 + if/switch → enum 状态机 → State 抽象接口 → 独立状态类 → 可测试的业务状态机 ↓ ↓ ↓ ↓ ↓ 简单直接 结构稍清楚 行为开始解耦 迁移规则更清晰 适合复杂系统 ``` 记住这 3 句话: > **状态模式的重点不是“保存状态”,而是“让状态决定行为”。** > **当同一个动作在不同状态下表现不同,状态模式通常比 `switch(state)` 更自然。** > **策略是外部选算法,状态是内部状态塑造行为。**