有限状态机与状态模式详解_FSM建模Java状态模式与C++表驱动模板实践
订单待支付才能付款、协议握手未完成不能发数据、工单已关闭不能再编辑------这类 「阶段不同、允许的操作不同」 的需求,用一长串 if (status == ...) 维护,很快就会 难读、难测、难扩展。
有限状态机(FSM) 用 状态、事件、转移 把规则写清楚;状态模式 则用 多态 把「在某个状态下能做什么」拆到不同类里。二者描述的是同一件事,实现路径不同。下文先讲 怎么建模 ,再用 Java 订单 说明状态模式,最后给出一套 C++17 表驱动 实现:子类用 enum class 定义状态与事件 ,在构造函数里 addTransition 注册边 ,运行时 trigger 驱动迁移 ,并支持 Guard、进入/退出钩子、互斥锁、Graphviz 导出。
文中 C++ 代码以 header-only 的 fsm::StateMachine 为核心,可直接拷入工程。范围限于 单层 FSM (不含子状态机、SCXML、Boost.SML 等方案的对比);需要 层级状态、异步事件队列 时,应在同一套转移表思路上另行扩展。
目录
- [1. 入门:何时该引入状态机](#1. 入门:何时该引入状态机)
- [2. 入门:FSM 五要素与开发六步](#2. 入门:FSM 五要素与开发六步)
- [3. 原理:状态表与状态图](#3. 原理:状态表与状态图)
- [4. 原理:状态模式(GoF)](#4. 原理:状态模式(GoF))
- [5. 原理:表驱动 FSM vs 状态模式](#5. 原理:表驱动 FSM vs 状态模式)
- [6. 用法:C++ 表驱动模板(基础)](#6. 用法:C++ 表驱动模板(基础))
- [7. 用法:Guard、钩子、线程安全与可视化](#7. 用法:Guard、钩子、线程安全与可视化)
- [8. 用法:接入工程与测试](#8. 用法:接入工程与测试)
- [9. 工程习惯与常见坑](#9. 工程习惯与常见坑)
- [10. 延伸阅读](#10. 延伸阅读)
1. 入门:何时该引入状态机
| 信号 | 说明 |
|---|---|
| 同一变量「状态」驱动多处分支 | status 出现在许多 if/switch 中 |
| 非法组合频繁出现 | 「未支付却发货」类 bug 靠人工记规则 |
| 新增状态要改很多文件 | 每加一个枚举值要扫全局 |
| 需要可交付的状态图 | 产品、测试、研发要对齐 允许的路径 |
不必上状态机 :只有 2~3 个布尔标志 、转移规则极少且稳定------简单 enum + switch 即可。
2. 入门:FSM 五要素与开发六步
2.1 五要素
| 要素 | 含义 |
|---|---|
| 状态(State) | 系统在某一时刻所处的阶段 |
| 事件(Event) | 触发迁移的外部/内部信号(用户操作、定时器、ACK) |
| 转移(Transition) | 在某一状态下收到某事件后 进入下一状态 |
| 初始状态 | 创建实例后的起点 |
| 终止状态(可选) | 不再接受业务事件,或仅只读 |
2.2 开发六步(工程流程)
需求分析
列出状态与事件
画状态图或状态表
实现与单测
边界与非法事件测试
文档化与评审
- 需求分析:谁触发迁移?有无并发?
- 定义状态与事件:命名稳定、与产品文案一致。
- 状态图 / 状态表 :评审 漏边、死路、环。
- 实现:表驱动、状态模式或现成库。
- 测试 :合法路径 + 非法事件(应拒绝)。
- 文档化 :图与代码 同源维护 (见 §7
dumpDot)。
3. 原理:状态表与状态图
以 订单 为例(与后文代码一致):
| 当前状态 | 事件 | 下一状态 |
|---|---|---|
| Pending | Pay | Paid |
| Pending | Cancel | Cancelled |
| Paid | Ship | Shipped |
| Paid | Cancel | Cancelled |
| Shipped | Complete | Completed |
| Completed | * | (无) |
| Cancelled | * | (无) |
Pay
Cancel
Ship
Cancel
Complete
Pending
Paid
Cancelled
Shipped
Completed
表驱动 实现时,上表即 (from, event) → to 的 唯一真相来源;代码不应再散落第二套规则。
4. 原理:状态模式(GoF)
状态模式 把 「在某个状态下如何处理操作」 拆到 多个 State 子类 ,Context 只持有 当前 State 指针 并 委托。
OrderContext
-OrderState current
+pay()
+ship()
<<interface>>
OrderState
+pay(ctx)
+ship(ctx)
PendingState
PaidState
| 角色 | 职责 |
|---|---|
| Context | 对外 API;转发给 currentState |
| State 接口 | 声明各操作 |
| Concrete State | 合法则 换状态 ;非法则 抛异常 / 返回错误 |
4.1 Java 订单示例
状态接口 + 上下文:
java
public interface OrderState {
void pay(OrderContext ctx);
void ship(OrderContext ctx);
void complete(OrderContext ctx);
void cancel(OrderContext ctx);
}
public class OrderContext {
private OrderState current = new PendingState();
public void setState(OrderState state) { this.current = state; }
public void pay() { current.pay(this); }
public void ship() { current.ship(this); }
public void complete(){ current.complete(this); }
public void cancel() { current.cancel(this); }
}
Pending 状态 (其余 PaidState、ShippedState 等同理:合法 setState,非法 throw new IllegalStateException(...)):
java
public class PendingState implements OrderState {
@Override public void pay(OrderContext ctx) {
ctx.setState(new PaidState());
}
@Override public void ship(OrderContext ctx) {
throw new IllegalStateException("未支付,不能发货");
}
@Override public void complete(OrderContext ctx) {
throw new IllegalStateException("未支付,不能完成");
}
@Override public void cancel(OrderContext ctx) {
ctx.setState(new CancelledState());
}
}
特点 :消除 Context 里巨大的 switch(status) ;新增状态 加类 即可。代价是 类数量随状态线性增长。
5. 原理:表驱动 FSM vs 状态模式
| 维度 | 表驱动 FSM(转移表 + trigger) |
状态模式(多态 State 类) |
|---|---|---|
| 规则存放 | 集中 addTransition |
分散在各 State 类 |
| 可读性 | 状态图/表 一眼全貌 | 行为 贴近 OOP,跳转需翻类 |
| 扩展 | 加一行转移;复杂动作用 回调 | 加 新 State 类 |
| 适用 | C++ 嵌入、协议、工单、转移规则稳定 | Java 业务域、每状态行为差异大 |
| 可视化 | 易从表 导出 dot | 需额外从代码 反推 |
二者 语义等价 :都是 「当前状态 + 事件 → 下一状态」 ;选型看 语言生态与团队习惯。
为何 C++ 侧用 CRTP + 表驱动,而不是经典 State 模式的虚函数?
- 表驱动 :转移规则集中在
addTransition,与 状态图/状态表 同源,易dumpDot、易单测 非法边。 - CRTP :
dumpDot通过derived().stateName()拿到子类字符串映射,无运行时多态开销 ,也避免 「每个状态一个类」 在 C++ 服务端/嵌入式里造成的 类爆炸。 - 回调(Guard / onEnter / onExit) 承担「某状态下行为差异」,而不是为每个状态写子类。
下面给出 C++ 表驱动 模板的具体写法。
6. 用法:C++ 表驱动模板(基础)
6.1 设计目标
cpp
class OrderSM : public fsm::StateMachine<OrderSM, OrderState, OrderEvent> {
public:
OrderSM(); // 构造函数里注册 addTransition
};
// sm.trigger(OrderEvent::Pay);
- 模板参数 :自定义
enum class状态 / 事件; - 子类 :在构造函数中 注册转移;
trigger(event):按 当前状态 + 事件 查表迁移。
6.2 转移键的可哈希(必做)
std::unordered_map<std::pair<State, Event>, ...> 在标准库中 默认没有 std::hash<std::pair<...>>(C++20 前常见写法)。工程上应使用 显式键类型 + 哈希,例如:
cpp
// StateMachine.h --- 核心实现(namespace fsm)
#pragma once
#include <functional>
#include <mutex>
#include <string>
#include <fstream>
#include <unordered_map>
#include <utility>
namespace fsm {
template <typename State, typename Event>
struct TransitionKey {
State state{};
Event event{};
bool operator==(const TransitionKey& o) const {
return state == o.state && event == o.event;
}
};
template <typename State, typename Event>
struct TransitionKeyHash {
std::size_t operator()(const TransitionKey<State, Event>& k) const {
return std::hash<int>{}(static_cast<int>(k.state)) ^
(std::hash<int>{}(static_cast<int>(k.event)) << 1);
}
};
template <typename Derived, typename State, typename Event>
class StateMachine {
public:
using Guard = std::function<bool()>;
using Action = std::function<void()>;
explicit StateMachine(State initial) : current_(initial) {}
protected:
struct Rule {
State to{};
Guard guard{};
Action onExit{};
Action onEnter{};
};
void addTransition(State from, Event evt, State to,
Guard guard = nullptr,
Action onExit = nullptr,
Action onEnter = nullptr) {
transitions_[TransitionKey<State, Event>{from, evt}] =
Rule{to, std::move(guard), std::move(onExit), std::move(onEnter)};
}
public:
bool trigger(Event evt) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = transitions_.find(TransitionKey<State, Event>{current_, evt});
if (it == transitions_.end()) return false;
Rule& rule = it->second;
if (rule.guard && !rule.guard()) return false;
if (rule.onExit) rule.onExit();
current_ = rule.to;
if (rule.onEnter) rule.onEnter();
return true;
}
State state() const {
std::lock_guard<std::mutex> lock(mutex_);
return current_;
}
void dumpDot(const std::string& path) const {
std::ofstream f(path);
f << "digraph FSM {\n";
for (const auto& [key, rule] : transitions_) {
f << " " << derived().stateName(key.state)
<< " -> " << derived().stateName(rule.to)
<< " [label=\"" << derived().eventName(key.event) << "\"];\n";
}
f << "}\n";
}
protected:
Derived& derived() { return static_cast<Derived&>(*this); }
const Derived& derived() const { return static_cast<const Derived&>(*this); }
private:
mutable std::mutex mutex_;
State current_{};
std::unordered_map<
TransitionKey<State, Event>,
Rule,
TransitionKeyHash<State, Event>
> transitions_;
};
} // namespace fsm
上述 StateMachine 已包含 Guard、onEnter/onExit、std::mutex、dumpDot 。trigger 返回 false 表示 当前状态下没有这条边 ,或 Guard 未通过 ;若希望与 Java 一样抛异常,可在业务封装里包一层 triggerOrThrow。
6.3 订单状态机子类
cpp
// 生产环境建议:显式底层值 + 稳定映射,勿依赖枚举默认递增顺序
// enum class OrderState : std::uint8_t {
// Pending = 1, Paid = 2, Shipped = 3, Completed = 4, Cancelled = 5
// };
enum class OrderState { Pending, Paid, Shipped, Completed, Cancelled };
enum class OrderEvent { Pay, Ship, Complete, Cancel };
class OrderSM : public fsm::StateMachine<OrderSM, OrderState, OrderEvent> {
public:
OrderSM() : StateMachine(OrderState::Pending) {
addTransition(OrderState::Pending, OrderEvent::Pay, OrderState::Paid,
[this] { return canPay(); },
[] { /* onExit Pending */ },
[] { /* onEnter Paid */ });
addTransition(OrderState::Pending, OrderEvent::Cancel, OrderState::Cancelled);
addTransition(OrderState::Paid, OrderEvent::Ship, OrderState::Shipped);
addTransition(OrderState::Paid, OrderEvent::Cancel, OrderState::Cancelled);
addTransition(OrderState::Shipped, OrderEvent::Complete, OrderState::Completed);
}
bool canPay() const { return true; }
std::string stateName(OrderState s) const {
static const char* k[] = {"Pending","Paid","Shipped","Completed","Cancelled"};
return k[static_cast<int>(s)];
}
std::string eventName(OrderEvent e) const {
static const char* k[] = {"Pay","Ship","Complete","Cancel"};
return k[static_cast<int>(e)];
}
};
cpp
int main() {
OrderSM sm;
sm.trigger(OrderEvent::Pay);
sm.trigger(OrderEvent::Ship);
sm.trigger(OrderEvent::Complete);
sm.dumpDot("order_fsm.dot");
return 0;
}
7. 用法:Guard、钩子、线程安全与可视化
| 能力 | 用途 |
|---|---|
| Guard | 转移前条件(余额足够、权限 OK);失败则 不迁移 |
| onExit / onEnter | 离开旧状态 / 进入新状态时打日志、停定时器、发指标 |
std::mutex |
多线程共用一个状态机实例时保护 current_ 与表 |
dumpDot |
从 已注册边 生成 Graphviz,便于评审与文档 |
生成状态图(需安装 Graphviz):
bash
g++ -std=c++17 -I include examples/order_sm.cpp -o order_sm
./order_sm
dot -Tsvg order_fsm.dot -o order_fsm.svg
CRTP(Derived 模板参数) :dumpDot 通过 derived().stateName() 调用子类 枚举 → 字符串,避免基类写死业务枚举。
8. 用法:接入工程与测试
8.1 推荐目录(header-only)
include/StateMachine.h # 上文 fsm 命名空间
examples/order_sm.cpp
tests/order_sm_test.cpp # 可选:GoogleTest / Catch2
业务模块 #include "StateMachine.h" ,子类集中在 order_fsm.cpp 或 protocol_fsm.cpp ,不要把转移表散在多处 addTransition。
8.2 测试清单
| 用例 | 期望 |
|---|---|
合法链 Pay → Ship → Complete |
终态 Completed |
| Pending 上 Ship | trigger false 或业务层报错 |
| Completed 上任意事件 | 拒绝 |
| Guard 返回 false | 状态不变 |
并发 trigger(若多线程) |
无数据竞争(TSan 跑一遍) |
8.3 与 Java 状态模式对照迁移
| Java 状态模式 | C++ 表驱动 |
|---|---|
PendingState.pay() 内 setState(new PaidState()) |
addTransition(Pending, Pay, Paid, ...) |
IllegalStateException |
trigger 返回 false 或封装抛错 |
| 行为全在 State 类 | 复杂行为放进 onEnter 回调 或 Guard 绑定的成员函数 |
9. 工程习惯与常见坑
| 习惯 | 原因 |
|---|---|
| 一张状态表 / 一份 dot 作评审依据 | 避免产品与代码规则分叉 |
| 非法事件显式处理 | 不要静默忽略 trigger == false |
| 终态不再注册出边 | Completed/Cancelled 上任何事件都应失败 |
| Guard 里不做重逻辑 | 只做 可否迁移 判断,副作用放 onEnter |
| 枚举值稳定 | 持久化状态码与 枚举底层值 变更需迁移方案 |
常见坑:
unordered_map<pair<...>>无法编译 :必须用 §6.2 的 TransitionKey + Hash。- onExit 里再
trigger:易重入;宜 队列化事件 或禁止嵌套触发。 - 状态模式滥用:只有 2 个状态仍拆 5 个类 → 维护成本高于收益。
- 图与代码不同步 :改
addTransition后 重新dumpDot并提交 svg/dot。
10. 延伸阅读
- GoF《设计模式》 --- State 模式原文。
- UML 状态图 --- 与 Harel 层级状态图 的关系;子状态、并行区域需另建模型。
- Boost.SML 、Boost.Statechart --- 功能更全的 C++ 方案;上文模板侧重 零第三方依赖、转移表即文档。
- Linux进程状态详解_内核task_struct到应用层排障实践 (仓库内)--- 内核进程状态 与 应用层 FSM 不是同一套概念。
落地时注意 :持久化状态码应对 enum class 显式赋值 ,不要默认依赖 static_cast<int> 的递增顺序;dumpDot 只画出 已注册的边 ;mutex 只保护状态机内部字段,业务对象仍要各自加锁;上线前用 §8.2 的用例把 合法路径与非法事件 跑全。