有限状态机与状态模式详解 FSM建模Java状态模式与C++表驱动模板实践

有限状态机与状态模式详解_FSM建模Java状态模式与C++表驱动模板实践

订单待支付才能付款、协议握手未完成不能发数据、工单已关闭不能再编辑------这类 「阶段不同、允许的操作不同」 的需求,用一长串 if (status == ...) 维护,很快就会 难读、难测、难扩展

有限状态机(FSM)状态、事件、转移 把规则写清楚;状态模式 则用 多态 把「在某个状态下能做什么」拆到不同类里。二者描述的是同一件事,实现路径不同。下文先讲 怎么建模 ,再用 Java 订单 说明状态模式,最后给出一套 C++17 表驱动 实现:子类用 enum class 定义状态与事件 ,在构造函数里 addTransition 注册边 ,运行时 trigger 驱动迁移 ,并支持 Guard、进入/退出钩子、互斥锁、Graphviz 导出

文中 C++ 代码以 header-onlyfsm::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 开发六步(工程流程)

需求分析
列出状态与事件
画状态图或状态表
实现与单测
边界与非法事件测试
文档化与评审

  1. 需求分析:谁触发迁移?有无并发?
  2. 定义状态与事件:命名稳定、与产品文案一致。
  3. 状态图 / 状态表 :评审 漏边、死路、环
  4. 实现:表驱动、状态模式或现成库。
  5. 测试 :合法路径 + 非法事件(应拒绝)。
  6. 文档化 :图与代码 同源维护 (见 §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 状态 (其余 PaidStateShippedState 等同理:合法 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 、易单测 非法边
  • CRTPdumpDot 通过 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::mutexdumpDottrigger 返回 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.cppprotocol_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.SMLBoost.Statechart --- 功能更全的 C++ 方案;上文模板侧重 零第三方依赖、转移表即文档
  • Linux进程状态详解_内核task_struct到应用层排障实践 (仓库内)--- 内核进程状态应用层 FSM 不是同一套概念。

落地时注意 :持久化状态码应对 enum class 显式赋值 ,不要默认依赖 static_cast<int> 的递增顺序;dumpDot 只画出 已注册的边mutex 只保护状态机内部字段,业务对象仍要各自加锁;上线前用 §8.2 的用例把 合法路径与非法事件 跑全。

相关推荐
你的保护色1 小时前
【无标题】
java·服务器·网络
basketball6162 小时前
C++ 构造函数完全指南:从入门到进阶
java·开发语言·c++
淘矿人2 小时前
Claude辅助DevOps实践
java·大数据·运维·人工智能·算法·bug·devops
想唱rap2 小时前
IO多路转接之poll
服务器·开发语言·数据库·c++
小江的记录本2 小时前
【Java基础】泛型:泛型擦除、通配符、上下界限定(附《思维导图》+《面试高频考点清单》)
java·数据结构·后端·mysql·spring·面试·职场和发展
来恩10033 小时前
请求转发与响应重定向的使用
java
@杰克成3 小时前
Java学习30
java·开发语言·学习
次元工程师!3 小时前
LangFlow开发(三)—Bundles组件架构设计(3W+字详细讲解)
java·前端·python·低代码·langflow
落羽的落羽4 小时前
【算法札记】练习 | Week4
linux·服务器·数据结构·c++·人工智能·算法·动态规划