你的状态逻辑正在失控
订单系统。用户下单 → 待支付 → 已支付 → 待发货 → 已发货 → 已签收 → 已完成。看起来简单,但加上这些分支呢:支付超时自动取消、已发货可以退货、退款要走到单独的流程、管理员可以强制关闭。
大多数人的写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
func UpdateOrderStatus(order *Order, action string) error {
if order.Status == "pending_payment" {
if action == "pay" {
order.Status = "paid"
} else if action == "cancel" {
order.Status = "cancelled"
} else if action == "timeout" {
order.Status = "cancelled"
}
} else if order.Status == "paid" {
if action == "ship" {
order.Status = "shipped"
} else if action == "refund" {
order.Status = "refunding"
}
} else if order.Status == "shipped" {
if action == "confirm" {
order.Status = "completed"
} else if action == "return" {
order.Status = "returning"
}
}
// ... 还有更多分支
return nil
}
|
状态多了之后,这种代码会膨胀成几百行的 if-else 瀑布。每次加新状态,你都得在这座迷宫里找到所有相关分支,小心翼翼地改。漏改一个就是 Bug。
**有限状态机(FSM)**就是解决这个问题的设计模式:把状态、事件、转移规则显式化,用一张表描述全部流转逻辑。
FSM 的三个核心概念
- 状态(State):对象在生命周期中的某个阶段,如「待支付」「已发货」
- 事件(Event):触发状态变化的外部动作,如「支付」「发货」「签收」
- 转移(Transition):从某状态接收某事件后,切换到目标状态的规则
关键约束:一个状态收到一个事件,只有一个确定的目标状态(或者不允许该事件)。这正是 if-else 代码缺少的——if-else 允许同一个状态下对不同事件做任何处理,逻辑分散在各处,而FSM强制你集中声明所有规则。
手写一个FSM:转移表驱动
核心思路是用一张二维表描述「当前状态 + 事件 → 目标状态」的映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
package fsm
// State 表示一个状态
type State string
// Event 表示一个触发事件
type Event string
// Transition 描述一条转移规则
type Transition struct {
From State
Event Event
To State
Action func(ctx interface{}) error // 可选:转移时执行的副作用
}
// Machine 状态机本体
type Machine struct {
current State
transitions map[transitionKey]Transition
}
type transitionKey struct {
from State
event Event
}
func New(initial State, rules []Transition) *Machine {
m := &Machine{
current: initial,
transitions: make(map[transitionKey]Transition),
}
for _, t := range rules {
m.transitions[transitionKey{t.From, t.Event}] = t
}
return m
}
// Current 返回当前状态
func (m *Machine) Current() State { return m.current }
// Fire 触发一个事件
func (m *Machine) Fire(event Event, ctx interface{}) error {
key := transitionKey{m.current, event}
t, ok := m.transitions[key]
if !ok {
return fmt.Errorf("非法转移:状态 %s 不允许事件 %s", m.current, event)
}
// 执行副作用(如果有)
if t.Action != nil {
if err := t.Action(ctx); err != nil {
return fmt.Errorf("转移副作用失败: %w", err)
}
}
m.current = t.To
return nil
}
// CanTransit 检查当前状态是否允许某事件
func (m *Machine) CanTransit(event Event) bool {
_, ok := m.transitions[transitionKey{m.current, event}]
return ok
}
|
用FSM重写订单状态
把分散的 if-else 变成一张声明式规则表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
const (
StatePendingPayment State = "pending_payment"
StatePaid State = "paid"
StateShipped State = "shipped"
StateCompleted State = "completed"
StateCancelled State = "cancelled"
StateReturning State = "returning"
StateRefunded State = "refunded"
)
const (
EventPay Event = "pay"
EventShip Event = "ship"
EventConfirm Event = "confirm"
EventCancel Event = "cancel"
EventTimeout Event = "timeout"
EventRefund Event = "refund"
EventReturn Event = "return"
)
func NewOrderFSM() *Machine {
rules := []Transition{
{From: StatePendingPayment, Event: EventPay, To: StatePaid,
Action: func(ctx interface{}) error {
o := ctx.(*Order)
return chargePayment(o) // 实际扣款逻辑
}},
{From: StatePendingPayment, Event: EventCancel, To: StateCancelled},
{From: StatePendingPayment, Event: EventTimeout, To: StateCancelled},
{From: StatePaid, Event: EventShip, To: StateShipped},
{From: StatePaid, Event: EventRefund, To: StateRefunded},
{From: StateShipped, Event: EventConfirm, To: StateCompleted},
{From: StateShipped, Event: EventReturn, To: StateReturning},
{From: StateReturning, Event: EventRefund, To: StateRefunded},
}
return New(StatePendingPayment, rules)
}
// 业务调用:只需一行
func (o *Order) Pay() error {
return o.fsm.Fire(EventPay, o)
}
func (o *Order) Ship() error {
return o.fsm.Fire(EventShip, o)
}
|
对比之前的 if-else 代码:
- 新增状态:在 rules 切片里加几行,不碰现有代码
- 查规则:看 rules 这张表,一目了然
- 非法操作防护:Fire 不匹配时自动报错,不会误执行
- 副作用内聚:扣款、通知等逻辑跟转移规则绑定,不会遗漏
守卫条件:一个事件多种结果
实际场景中,同一事件在不同条件下可能转到不同状态。比如「退货」在已签收7天内允许,超过7天拒绝。加一个守卫函数:
1
2
3
4
5
6
7
|
type Transition struct {
From State
Event Event
To State
Guard func(ctx interface{}) bool // 守卫:返回false则跳过此规则
Action func(ctx interface{}) error
}
|
Fire 方法改为按优先级遍历匹配列表,第一个通过 Guard 的规则生效:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
func (m *Machine) Fire(event Event, ctx interface{}) error {
candidates := m.transitions[transitionKey{m.current, event}]
for _, t := range candidates {
if t.Guard != nil && !t.Guard(ctx) {
continue
}
if t.Action != nil {
if err := t.Action(ctx); err != nil {
return err
}
}
m.current = t.To
return nil
}
return fmt.Errorf("无匹配转移:状态 %s + 事件 %s", m.current, event)
}
// 注册带守卫的规则
rules := []Transition{
{From: StateCompleted, Event: EventReturn, To: StateReturning,
Guard: func(ctx interface{}) bool {
o := ctx.(*Order)
return time.Since(o.CompletedAt) < 7*24*time.Hour
}},
}
|
transitions 改为 map[transitionKey][]Transition 即可支持同一 key 多条规则。
持久化:状态要落库
FSM 运行在内存里,但订单状态得存数据库。做法很简单:只持久化当前状态值,下次加载时用 New(savedState, rules) 重建状态机:
1
2
3
4
5
6
7
8
9
10
11
12
|
// 从数据库加载
order := db.GetOrder(id)
order.fsm = New(State(order.DBStatus), rules)
// 触发事件后,把新状态写回数据库
func (o *Order) Pay() error {
if err := o.fsm.Fire(EventPay, o); err != nil {
return err
}
o.DBStatus = string(o.fsm.Current())
return db.SaveOrder(o)
}
|
规则是声明式的、无状态的,每次从代码重建即可,不需要序列化。
什么时候该用FSM,什么时候别用
该用:
- 对象有明确的生命周期阶段(订单、工单、审批流、游戏角色)
- 状态数 ≥ 4,且事件会触发状态切换
- 非法状态转移需要被拦截
别用:
- 状态只有「开/关」两个——直接布尔值就够了
- 状态流转极度自由(任意状态可转任意状态)——FSM 的约束反而碍事
- 状态本身是数据驱动的连续值(如温度、进度百分比)——这不是状态机的场景
开源库推荐
不想自己造轮子的话:
- Go:
github.com/looplab/fsm — 轻量,支持回调、守卫,社区活跃
- Python:
transitions 库 — 功能丰富,支持嵌套状态、定时器
- Java:Spring StateMachine — 与 Spring 生态深度集成,适合企业级流程
自造 FSM 的好处是完全可控、零依赖,核心代码不超过 100 行。如果你的状态逻辑确实复杂到需要嵌套状态、并行区域等高级特性,再引入库不迟。
小结
if-else 瀑布不是起点烂,是长大了之后不可维护。FSM 的价值不在于复杂,而在于把隐式逻辑显式化:状态、事件、转移规则集中声明,改一个分支不需要搜索整个代码库。当你发现自己在写第四层 else if 来判断同一个对象的状态时,就是引入状态机的信号。