状态模式这个经典设计模式,在C++里讲的人很多,但真正把它落地到项目里、能扛住真实业务需求的例子却不多。大多数教程停留在“定义接口、写几个子类、Context里存一个指针”这种教科书级别,看完感觉懂了,真到自己写业务代码时又不知道怎么下手。这篇文章我不讲虚的,直接用三个完整案例——一个游戏角色状态、一个订单状态机、一个基于现代C++的std::variant写法,把状态模式从原理到坑全部过一遍。适合已经会用C++写类、懂虚函数和继承、但想让代码结构更清晰的开发者,如果你是刚学完语法想看设计模式怎么落地的,也能从中拿到可以直接抄的代码。
1. 状态模式到底解决什么问题
1.1 一个让switch失控的真实场景
先看一段我在实际项目里见过无数次的老代码。假设你要做一个玩家角色控制,角色有站立、跑步、跳跃、攻击四种状态,每种状态下对键盘输入的反应不一样:
cpp复制enum class PlayerState {
Idle, Run, Jump, Attack
};
void Player::handleInput(const std::string& input) {
switch (m_state) {
case PlayerState::Idle:
if (input == "run") {
m_state = PlayerState::Run;
m_speed = 5.0f;
} else if (input == "jump") {
m_state = PlayerState::Jump;
m_velocity = 10.0f;
}
break;
case PlayerState::Run:
if (input == "stop") {
m_state = PlayerState::Idle;
m_speed = 0.0f;
} else if (input == "jump") {
m_state = PlayerState::Jump;
m_velocity = 8.0f;
}
break;
case PlayerState::Jump:
if (input == "attack") {
// 跳跃中还能攻击?
m_state = PlayerState::Attack;
}
break;
case PlayerState::Attack:
if (input == "release") {
m_state = PlayerState::Idle;
}
break;
}
}
这段代码看着不难,但问题会随着状态和事件数量增长迅速失控。四个状态、四类事件,switch-case还能勉强应付。可一旦你加入受伤、防御、技能释放、硬直、死亡,每个状态下对同一事件的响应都不一样,switch分支数量成指数膨胀,逻辑开始互相牵扯,改一个状态的行为,可能不小心影响另外三个状态。
1.2 状态混乱的三个典型症状
结合我自己的经验,这种if-else和switch堆出来的状态逻辑,通常会表现出三个典型症状。
第一个症状是“每个函数里都有一堆switch”。最开始只要handleInput一个函数,后来发现角色要支持动画播放、碰撞检测、属性计算,每个函数都要根据当前状态走不同的分支。代码重复严重,加一个状态意味着要把所有相关函数都翻一遍,漏改一个就是莫名其妙的bug。
第二个症状是“状态转移逻辑散落在各处”。状态何时切换、切换后要先做什么重置,这些规则藏在业务代码的各个角落。你很难回答“角色到底能不能从攻击状态直接跳到跑步状态”这个问题,因为答案要看代码执行到哪个switch分支。
第三个症状是“状态相关的数据缺少归属”。跑步状态需要m_speed,跳跃状态需要m_velocity,这些临时数据全部堆在角色类里,每个状态只用了其中一小部分。类成员越来越多,对象体积膨胀,初始化逻辑复杂,还容易在状态切换时忘记清理上一个状态遗留的数据。
1.3 状态模式的核心思路:把每种状态封装成对象
状态模式解决这类问题的思路,是把“状态”从零散的枚举值升级为“对象”。每个具体状态是一个类,它知道自己在这个状态下怎么响应事件、转移到下一个状态时需要做什么清理。角色类(Context)不再维护一堆switch分支,只持有一个当前状态对象的指针,把事件转交给这个对象处理。
这个转变的本质是:把“基于状态的分支逻辑”变成了“基于多态的动态分发”。业务代码不再需要知道有多少种状态,新增状态也不需要修改现有代码,符合开闭原则。状态相关数据天然归位到各自的状态类里,不会互相污染。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 经典状态模式的C++实现:手写一个游戏角色
2.1 最小骨架:State基类与Player上下文
经典状态模式在C++里首先是抽象基类加一个Context类。下面这个示例我用游戏角色控制来演示,代码不复杂,但五脏俱全。
cpp复制#include <iostream>
#include <memory>
#include <string>
class Player;
// 状态基类:所有具体状态的接口
class State {
public:
virtual ~State() = default;
// 处理输入,可能需要切换Player的状态
virtual void handleInput(Player& player, const std::string& input) = 0;
// 每帧更新,用于状态内持续的逻辑(比如跳跃下落)
virtual void update(Player& player, float deltaTime) {}
virtual std::string name() const = 0;
protected:
// 给子类用的辅助函数,后面说为什么要放在这里
void changeState(Player& player, std::unique_ptr<State> newState);
};
class Player {
public:
Player() : m_state(std::make_unique<IdleState>()) {}
void handleInput(const std::string& input) {
m_state->handleInput(*this, input);
}
void update(float deltaTime) {
m_state->update(*this, deltaTime);
}
void changeState(std::unique_ptr<State> newState) {
m_state = std::move(newState);
std::cout << "State switched to: " << m_state->name() << std::endl;
}
std::string stateName() const { return m_state->name(); }
// 角色属性,跨状态共享的数据放在这里
float m_speed = 0.0f;
float m_velocity = 0.0f;
int m_attackCombo = 0;
private:
std::unique_ptr<State> m_state;
};
void State::changeState(Player& player, std::unique_ptr<State> newState) {
player.changeState(std::move(newState));
}
这里有个设计细节值得说明:changeState我同时写在了Player和State里。为什么State里还要包一层?因为子类在handleInput里需要触发状态切换,如果直接调用player.changeState,那这个函数就必须是public的。把切换动作封装到State的protected方法里,对外暴露的接口就只有handleInput和update,Context的接口更干净,也隐含了“只有状态类能触发状态切换”的语义约束。
2.2 具体状态实现:Idle、Run、Jump、Attack
接下来实现四个具体状态,重点看每个状态如何管理自己的数据、如何转移:
cpp复制class IdleState : public State {
public:
void handleInput(Player& player, const std::string& input) override {
if (input == "run") {
player.m_speed = 5.0f;
changeState(player, std::make_unique<RunState>());
} else if (input == "jump") {
player.m_velocity = 8.0f;
changeState(player, std::make_unique<JumpState>());
}
}
std::string name() const override { return "Idle"; }
};
class RunState : public State {
public:
void handleInput(Player& player, const std::string& input) override {
if (input == "stop") {
player.m_speed = 0.0f;
changeState(player, std::make_unique<IdleState>());
} else if (input == "jump") {
player.m_velocity = 6.0f; // 跑步中起跳速度更低
changeState(player, std::make_unique<JumpState>());
}
}
void update(Player& player, float /*deltaTime*/) override {
// 跑步状态下持续前进
player.m_speed = 5.0f;
}
std::string name() const override { return "Run"; }
};
class JumpState : public State {
public:
void handleInput(Player& player, const std::string& input) override {
if (input == "attack") {
// 空中攻击打断跳跃
player.m_attackCombo = 1;
changeState(player, std::make_unique<AttackState>());
}
}
void update(Player& player, float deltaTime) override {
// 模拟重力,跳跃是“临时状态”,落回地面回到Idle
player.m_velocity -= 9.8f * deltaTime;
player.m_y += player.m_velocity * deltaTime;
if (player.m_y <= 0.0f) {
player.m_y = 0.0f;
player.m_velocity = 0.0f;
changeState(player, std::make_unique<IdleState>());
}
}
std::string name() const override { return "Jump"; }
};
class AttackState : public State {
public:
void handleInput(Player& player, const std::string& input) override {
if (input == "release") {
player.m_attackCombo = 0;
changeState(player, std::make_unique<IdleState>());
}
}
std::string name() const override { return "Attack"; }
};
留意JumpState里的update,它本身就是“状态内持续逻辑”的典型。如果没有状态模式,这种重力模拟 + 落地检测的代码写在哪里都不太对,写在Player里就得判断“当前是不是跳跃状态”,有了状态模式,它就自然属于JumpState自己。
2.3 状态转移放哪:状态内触发 vs 上下文统一调度
上面示例中我选择了“状态内触发转移”这种设计,也就是每个状态在处理输入时自行决定下一个状态是谁。这是状态模式最常见用法,优点是新状态自己说了算,逻辑聚拢,缺点也很明显:状态之间产生了隐式耦合——状态类需要知道下一个状态类的具体类型,才能构造出来。这也是为什么很多状态模式实现里,状态类要相互包含头文件,处理不好就是循环依赖。
另一个设计流派是“上下文统一调度”,也就是Context里维护一张状态转移表。比如用std::map<std::pair<StateId, EventId>, StateId>存储转移规则,事件进来后查表决定下一个状态。这种方式的优点是状态类之间完全不互相依赖,转移规则集中可审计,缺点是事件处理逻辑(进入状态前/离开状态后的动作)被拆到上下文或状态工厂里,状态类本身变薄,更像数据而不是逻辑。
两种方案没有绝对优劣。我的经验是:状态数量在10个以内、转移规则基本固定,用“状态内触发”最自然,代码读起来像故事;状态数量多、转移规则复杂且经常调整(比如工作流引擎、订单状态机),用“转移表 + 状态工厂”更稳,因为规则可视、可配置、可测试。
2.4 状态对象生命周期:每次都new还是复用单例
状态模式的经典UML图里,Context和State是聚合关系,并没有规定状态对象怎么创建。实践中有两种选择。
第一种是“每次切换都创建新状态对象”。上面示例就是这样,std::make_unique<RunState>()用完即弃。好处是实现简单,每个状态实例天然是“干净”的,不会残留上一次的数据;坏处是频繁切换时有分配开销,但现代C++里make_unique的分配开销极小,除非你在游戏每帧切换上万次,否则完全不用考虑性能问题。
第二种是“状态对象预创建并复用”,典型做法是让所有状态对象成为Player的成员或单例。好处是零分配开销,状态对象可以存一些跨切换保留的配置数据;坏处是状态对象里有可变数据时容易串状态——如果RunState里有个累计跑步时间的变量,复用时需要手动重置,忘了就是bug。
我的建议:默认用“每次创建”,把状态对象当值对象用;只有当你明确测出状态切换是性能瓶颈,或者状态对象持有复用成本很高的资源(数据库连接、文件句柄)时,再考虑复用的方案。
3. 现代C++的另一种解法:std::variant状态机
3.1 为什么还要聊std::variant方案
经典状态模式用虚函数实现多态,写起来中规中矩,但有一个不那么舒服的点:状态类通常会很多,每个类都不大、只在转移时动一下,类数量膨胀会拖慢编译时间。而且在C++里,虚函数调用本身是间接跳转,状态切来切去时对象在堆上反复创建,对追求性能的场景不算最优。
C++17引入了std::variant,配合std::visit,可以写出一种“静态多态”的状态机。思路是用std::variant<StateA, StateB, StateC>保存当前状态,用std::visit在编译期生成分支分发,替代虚函数表的运行时查找。同时每个状态对象直接存在variant里,不涉及堆分配,缓存友好,性能相当有优势。
这里要说明一下:variant方案在语义上其实更接近“枚举值 + 全局分发函数”的封装升级,没有了“状态对象自己转移自己”的职责划分,但换来了代码量更少、编译期检查更强的好处。它不是状态模式的正统替代品,但在很多场景下是更实用的写法。
3.2 用std::variant重写游戏角色
我来重写前面的角色控制,感受一下两种写法的差别:
cpp复制#include <iostream>
#include <variant>
// 前向声明,避免循环依赖
struct PlayerData {
float speed = 0.0f;
float velocity = 0.0f;
float y = 0.0f;
int attackCombo = 0;
};
struct Idle {};
struct Run {};
struct Jump {
float height = 0.0f;
};
struct Attack {
int comboCount = 0;
};
using PlayerState = std::variant<Idle, Run, Jump, Attack>;
// 事件处理:每种状态对输入的反应
struct HandleInput {
PlayerData& data;
PlayerState& state;
const std::string& input;
void operator()(Idle&) const {
if (input == "run") {
data.speed = 5.0f;
state = Run{};
} else if (input == "jump") {
data.velocity = 8.0f;
state = Jump{};
}
}
void operator()(Run&) const {
if (input == "stop") {
data.speed = 0.0f;
state = Idle{};
} else if (input == "jump") {
data.velocity = 6.0f;
state = Jump{};
}
}
void operator()(Jump&) const {
if (input == "attack") {
data.attackCombo = 1;
state = Attack{};
}
}
void operator()(Attack&) const {
if (input == "release") {
data.attackCombo = 0;
state = Idle{};
}
}
};
HandleInput是一个struct,里面针对variant的每个备选类型重载了operator(),std::visit(HandleInput{...}, state)时会自动选择匹配当前状态下标的重载。这就是静态多态:所有分支在编译期就确定了,运行时没有虚表间接跳转和堆分配。
3.3 两种方案怎么选:一张表讲清楚
我用过这两种方案写过多套业务代码,把它们放在一起对比:
| 维度 | 经典状态模式(虚函数) | std::variant状态机 |
|---|---|---|
| 运行性能 | 虚函数间接调用 + 堆分配 | 编译期分发 + 栈上存储 |
| 新增状态成本 | 新写一个类,侵入Context | 类型列表加一个项,补visit重载 |
| 状态内数据 | 天然封装在状态类成员 | 数据需要挂在全局结构体或状态struct里 |
| 代码量 | 类数量多,代码分散 | 更紧凑,但函数对象体内联变大 |
| 状态转移规则可视化 | 散落在各状态类 | 可以集中在visit函数里,便于审计 |
| 依赖关系 | 容易循环依赖 | 结构体之间几乎无依赖 |
| 适用场景 | 状态逻辑复杂、每个状态有大量方法 | 状态数量适中、转移/事件逻辑集中 |
我的选择倾向是:如果每个状态除了“响应事件”还有多个行为(进入时初始化、每帧更新、退出时清理),用经典状态模式,因为状态对象自己持有数据,职责归属清晰;如果状态主要是“数据 + 事件响应逻辑”,没有太重的update行为,用variant方案,性能和可维护性都好很多。
4. 实战案例:从零实现一个订单状态机
4.1 需求定义:订单的5个状态与9种事件
游戏角色的例子偏向演示,下面这个订单状态机更接近业务系统。需求如下:订单状态包括待支付、已支付、已发货、已完成、已取消,事件包括提交订单、支付成功、发货、确认收货、取消订单、超时取消、退款、重新支付、关闭订单。
先梳理合法转移关系,我习惯用一张小表钉在代码注释里,避免后续开发凭感觉改状态:
| 当前状态 | 合法事件 | 后继状态 |
|---|---|---|
| 待支付 | 支付成功 | 已支付 |
| 待支付 | 取消订单 | 已取消 |
| 待支付 | 超时取消 | 已取消 |
| 已支付 | 发货 | 已发货 |
| 已支付 | 退款 | 已取消 |
| 已发货 | 确认收货 | 已完成 |
| 已支付 | 重新支付(不发生转移) | 已支付 |
| 已完成 | 关闭订单 | 已完成 |
这张表的意义在于:状态转移不是“随便跳”,每条规则背后都有业务语义。写代码前先把规则列清楚,能避免很多无谓的返工。
4.2 用经典状态模式实现一个订单上下文
这里我设计一个OrderContext,持有当前状态,同时记录订单编号、金额等公共数据。具体状态类只负责判断某个事件是否在当前状态下合法,合法则转移并执行进入动作。
cpp复制#include <iostream>
#include <memory>
#include <string>
#include <vector>
enum class OrderEvent {
Submit, PaySuccess, Cancel, Timeout, Ship, Confirm, Refund, Close
};
std::string eventName(OrderEvent e) {
static const char* names[] = {"Submit", "PaySuccess", "Cancel", "Timeout", "Ship", "Confirm", "Refund", "Close"};
return names[static_cast<int>(e)];
}
class OrderContext;
class OrderState {
public:
virtual ~OrderState() = default;
virtual void handleEvent(OrderContext& ctx, OrderEvent ev) {
// 默认行为:当前状态不处理该事件,忽略或记录日志
std::cout << "Ignore event " << eventName(ev)
<< " in state " << stateName() << std::endl;
}
virtual std::string stateName() const = 0;
protected:
void transitTo(OrderContext& ctx, std::unique_ptr<OrderState> next);
};
transitTo放到State的protected里,让具体状态类在确认合法性后调用。
cpp复制class OrderContext {
public:
OrderContext(int orderId, double amount)
: m_orderId(orderId), m_amount(amount),
m_state(std::make_unique<PendingPayState>()) {}
void handleEvent(OrderEvent ev) {
m_state->handleEvent(*this, ev);
}
std::string currentState() const { return m_state->stateName(); }
void changeState(std::unique_ptr<OrderState> next) {
m_state = std::move(next);
std::cout << "Order " << m_orderId << " -> " << m_state->stateName() << std::endl;
}
int m_orderId;
double m_amount;
private:
std::unique_ptr<OrderState> m_state;
};
void OrderState::transitTo(OrderContext& ctx, std::unique_ptr<OrderState> next) {
ctx.changeState(std::move(next));
}
然后是具体状态类。我写了待支付、已支付、已发货、已完成、已取消五个,核心逻辑在每个状态的handleEvent里:
cpp复制class PendingPayState : public OrderState {
public:
void handleEvent(OrderContext& ctx, OrderEvent ev) override {
switch (ev) {
case OrderEvent::PaySuccess:
transitTo(ctx, std::make_unique<PaidState>());
break;
case OrderEvent::Cancel:
case OrderEvent::Timeout:
transitTo(ctx, std::make_unique<CancelledState>());
break;
default:
OrderState::handleEvent(ctx, ev);
}
}
std::string stateName() const override { return "PendingPay"; }
};
class PaidState : public OrderState {
public:
void handleEvent(OrderContext& ctx, OrderEvent ev) override {
switch (ev) {
case OrderEvent::Ship:
transitTo(ctx, std::make_unique<ShippedState>());
break;
case OrderEvent::Refund:
transitTo(ctx, std::make_unique<CancelledState>());
break;
case OrderEvent::PaySuccess:
// 重复支付成功,业务上忽略,或可以做退款处理
std::cout << "Duplicate pay, refund maybe needed" << std::endl;
break;
default:
OrderState::handleEvent(ctx, ev);
}
}
std::string stateName() const override { return "Paid"; }
};
class ShippedState : public OrderState {
public:
void handleEvent(OrderContext& ctx, OrderEvent ev) override {
switch (ev) {
case OrderEvent::Confirm:
transitTo(ctx, std::make_unique<CompletedState>());
break;
default:
OrderState::handleEvent(ctx, ev);
}
}
std::string stateName() const override { return "Shipped"; }
};
class CompletedState : public OrderState {
public:
void handleEvent(OrderContext& ctx, OrderEvent ev) override {
switch (ev) {
case OrderEvent::Close:
// 已完成单可关闭,关闭后仍是已完成
std::cout << "Order closed" << std::endl;
break;
default:
OrderState::handleEvent(ctx, ev);
}
}
std::string stateName() const override { return "Completed"; }
};
class CancelledState : public OrderState {
public:
void handleEvent(OrderContext& ctx, OrderEvent ev) override {
// 已取消状态几乎不处理任何转移
OrderState::handleEvent(ctx, ev);
}
std::string stateName() const override { return "Cancelled"; }
};
主程序验证一下流程:
cpp复制int main() {
OrderContext order(1001, 299.0);
order.handleEvent(OrderEvent::PaySuccess); // 待支付 -> 已支付
order.handleEvent(OrderEvent::Ship); // 已支付 -> 已发货
order.handleEvent(OrderEvent::Confirm); // 已发货 -> 已完成
order.handleEvent(OrderEvent::Ship); // 已完成状态下发货被忽略
return 0;
}
输出类似:
code复制Order 1001 -> Paid
Order 1001 -> Shipped
Order 1001 -> Completed
Ignore event Ship in state Completed
这个例子最典型的价值是“防呆”:业务系统最怕非法状态跳转,一个已取消的订单被人手一抖发货了,后果很严重。状态模式把每个状态的合法事件和非法事件都写死在对应状态类里,非法事件统一走基类的默认忽略逻辑,从架构上杜绝了“状态乱跳”。
4.3 状态转移合法表:写在代码里的业务契约
我在上面的例子里用文字列了一张转移表,实际项目中我更推荐把它做成代码注释,例如在每个状态类头部写上“合法事件:xxx,后继状态:yyy”。这样做的好处是让后来维护代码的人一眼看到该状态允许什么,不需要翻接口文档或猜业务逻辑。
更进一步,如果你想要可测试、可审计的状态机,可以把转移表抽离成单独数据结构,接收(当前状态, 事件)后查表返回后继状态,状态类里只保留进入/退出动作。我自己做工作流引擎时就用这种方案,状态表存数据库,改流程不动代码,上线审批流程做到了运营可配置。
4.4 扩展:给订单状态机加一个“退款中”状态
状态机从设计上讲就是用来应对业务变化的。假设产品经理说“已支付订单申请退款后不直接取消,先进入退款中,退款审批通过才取消”。这个需求在原有代码上扩展,只需要三步:
- 新增
RefundingState类,继承OrderState; - 在
PaidState的Refund事件分支里,把转移目标从CancelledState改成RefundingState; - 在
RefundingState里处理“审批通过 -> 已取消”、“审批驳回 -> 回到已支付”。
整个过程中OrderContext和其余状态类完全不需要修改,这就是状态模式开闭原则的直观体现。相比之下,如果用原始switch-case,改动会牵扯到每个case里的转移分支,稍不留神就漏了某个状态的处理。
5. 状态模式在C++里的坑与经验
5.1 循环依赖怎么破:前向声明还是拆接口
状态模式里Context和State天然互相引用:Context持有State,State的转移动作又需要操作Context。在不做设计的代码里,这容易变成“Header A include Header B,Header B include Header A”,编译直接报错。
常规解法是“最小化include + 前向声明”。Context的头文件里用#include <memory>和一个前置声明class State;,就能在成员变量里使用std::unique_ptr<State>,不需要包含State的完整定义。State的头文件同理,前置声明class OrderContext;也能声明void handleEvent(OrderContext& ctx, ...)。只有在handleEvent的实现文件(.cpp)里才需要包含对方的头文件。
另一个不太常用但值得了解的思路是“接口下沉”:把Context抽象成接口(例如ContextInterface),State只依赖接口,不依赖具体Context。这样State的头文件只include接口就可以,实现层的Context负责实现接口。缺点是代码层数多一层,小项目没必要。
5.2 状态重复转移与死循环问题
状态模式里常见的bug之一是“状态在自己状态内不断循环转移”。比如AttackState的handleInput里,如果输入依然是“attack”,又把自己转移到新的AttackState实例。每次转移都打印日志、可能触发进入动作,状态没变但副作用不断产生,从日志上看就像状态在“抖动”。
我排查这类问题的经验是:在changeState里加一个保护判断,如果新旧状态类型相同,就不执行转移动作:
cpp复制void OrderContext::changeState(std::unique_ptr<OrderState> next) {
if (m_state->stateName() == next->stateName()) {
// 同状态转移,可能是重复事件,记录一下就好
std::cout << "Same state transfer ignored: " << m_state->stateName() << std::endl;
return;
}
m_state = std::move(next);
}
但要注意这个判断依赖stateName(),如果两个不同状态类返回相同名字,这个保护会误杀。相比之下,用std::variant的方案如果转移到相同类型,std::visit仍会触发一次调用,但状态本身是值对象,重复赋值不会有“new新对象”的堆分配成本,危害小很多。
5.3 状态对象生命周期与悬垂指针
使用“每次创建新状态对象”方案时,要警惕一种隐蔽的悬垂指针问题。假如某个状态在handleEvent里获取了指向自身或Context内部数据的裸指针/引用,然后又触发状态转移,旧状态对象被析构,那个裸指针就悬垂了。这个问题在经典C++98风格的状态模式代码里很常见。
解决思路有两个:一是规则上约定“状态处理完事件后不要再触碰任何状态相关的引用”,这需要纪律;二是把状态切换放在事件处理函数的末尾,并且禁止在状态中保存长期有效的自引用。如果你用了std::shared_ptr管理状态,虽然能避免对象释放问题,但也带来了循环引用风险——Context持有State,State若要持回Context,就会形成环,需要weak_ptr打断。所以我的建议是:优先用unique_ptr + 前置声明,让所有权方向单一(Context拥有State),State不反向持有Context。
5.4 线程安全:状态机能不能多线程跑
订单状态机如果跑在业务系统里,很可能被多个线程同时触发事件,比如支付回调线程在“支付成功”,用户点击线程在“取消订单”。如果不加保护,两个线程同时读当前状态、同时决定转移,状态就乱了。
最简单的方案是对整个handleEvent加锁,保证同一时间只有一个事件被处理。C++里用std::mutex包住入口即可:
cpp复制void OrderContext::handleEvent(OrderEvent ev) {
std::lock_guard<std::mutex> lock(m_mutex);
m_state->handleEvent(*this, ev);
}
如果并发要求很高,再考虑“状态机无锁化”——把状态转移设计成纯函数,输入当前状态和事件,输出后继状态,用原子变量替换状态引用。这个路子比较深,一般业务系统用不到,但如果你在游戏服务器里做角色状态机,值得研究。
5.5 状态模式不是银弹:什么时候别硬上
状态模式写多了容易上瘾,什么逻辑都想套一层状态。这里泼一盆冷水:如果你的状态只有两三个,转移规则小于五条,用switch-case反而更简单直接。状态模式引入的类数量、间接调用、代码分散,对简单场景是净负担。
另外,如果状态之间几乎没有行为差异,只是数据不同,那更适合用一个结构体加配置表来描述,而不是用继承体系。例如“用户权限等级”,等级A和等级B的差别只是几个权限标志,这用枚举加查表就够了,写成状态模式纯属过度设计。判断标准我总结成一句话:当状态的行为差异足够大、且每种状态有自己的数据和逻辑时,状态模式才值得用;如果状态只是起了个名字的枚举值,就别硬上。
最后分享一点我的实际体会
我做了几年C++开发,状态模式是少数几个我在不同项目里反复用到的设计模式之一——从游戏角色到业务订单,从网络协议解析到UI界面切换,本质上都是在处理“状态 + 事件”这个永恒的组合。写这篇文章时我把自己踩过的坑也整理了进去,最想说的一点是:设计模式的代码永远比概念图复杂,但复杂不是坏事,真正的收益在于当业务需求变化时,你能用最小代价扩展系统,而不是拿着一堆if-else和同事一起焦头烂额地找那个漏掉的case。如果你现在手上正好有一个状态越来越乱的C++项目,不妨挑一个最痛的部分,用状态模式或std::variant重写一遍,你会很快感受到这两种写法的差别。
