4. 状态机中的动作执行

任务说明

在状态机中,状态转换往往需要伴随副作用:进入新状态时发送通知、离开旧状态时清理资源、转换过程中更新业务数据。本案例以“订单状态机”为最小可复现场景,实现进入动作、退出动作和转换动作的挂载与触发机制,覆盖“待支付 → 已支付 → 已发货 → 已完成”的典型流转。

核心实现

// order_state_machine.go
package main

import (
    "fmt"
    "strings"
)

// 订单状态定义
type OrderState int

const (
    PendingPayment OrderState = iota // 待支付
    Paid                             // 已支付
    Shipped                          // 已发货
    Completed                        // 已完成
)

func (s OrderState) String() string {
    switch s {
    case PendingPayment:
        return "待支付"
    case Paid:
        return "已支付"
    case Shipped:
        return "已发货"
    case Completed:
        return "已完成"
    }
    return "未知"
}

// 动作类型
type ActionType int

const (
    OnEnter ActionType = iota // 进入动作
    OnExit                    // 退出动作
    OnTransition              // 转换动作
)

// 订单结构
type Order struct {
    ID       string
    State    OrderState
    Stock    int  // 库存余量
    Notified bool // 是否已发送通知
}

// 状态机定义
type StateMachine struct {
    order    *Order
    enterFn  map[OrderState]func(*Order)
    exitFn   map[OrderState]func(*Order)
    transFn  map[[2]OrderState]func(*Order)
}

// 新建状态机并注册动作
func NewOrderStateMachine(o *Order) *StateMachine {
    sm := &StateMachine{
        order:   o,
        enterFn: make(map[OrderState]func(*Order)),
        exitFn:  make(map[OrderState]func(*Order)),
        transFn: make(map[[2]OrderState]func(*Order)),
    }

    // 注册进入动作
    sm.enterFn[Paid] = func(o *Order) {
        o.Notified = true
        fmt.Printf("[通知] 订单 %s 已支付,通知用户\n", o.ID)
    }
    sm.enterFn[Shipped] = func(o *Order) {
        fmt.Printf("[通知] 订单 %s 已发货,通知用户\n", o.ID)
    }

    // 注册退出动作
    sm.exitFn[PendingPayment] = func(o *Order) {
        fmt.Printf("[清理] 订单 %s 离开待支付,取消未支付定时器\n", o.ID)
    }
    sm.exitFn[Paid] = func(o *Order) {
        fmt.Printf("[清理] 订单 %s 离开已支付,关闭支付确认页\n", o.ID)
    }

    // 注册转换动作(更新库存)
    sm.transFn[[2]OrderState{PendingPayment, Paid}] = func(o *Order) {
        o.Stock -= 1
        fmt.Printf("[库存] 订单 %s 支付成功,扣减库存,剩余 %d\n", o.ID, o.Stock)
    }

    return sm
}

// 执行状态转换
func (sm *StateMachine) TransitionTo(next OrderState) error {
    current := sm.order.State
    if current == next {
        return fmt.Errorf("状态未变化: %s", current)
    }

    // 1. 执行当前状态的退出动作
    if fn, ok := sm.exitFn[current]; ok {
        fn(sm.order)
    }

    // 2. 执行转换动作
    key := [2]OrderState{current, next}
    if fn, ok := sm.transFn[key]; ok {
        fn(sm.order)
    }

    // 3. 更新状态
    sm.order.State = next

    // 4. 执行新状态的进入动作
    if fn, ok := sm.enterFn[next]; ok {
        fn(sm.order)
    }

    return nil
}

func main() {
    o := &Order{ID: "A1001", State: PendingPayment, Stock: 5}
    sm := NewOrderStateMachine(o)

    fmt.Printf("初始状态: %s, 库存: %d\n", o.State, o.Stock)

    // 执行转换链
    sm.TransitionTo(Paid)
    sm.TransitionTo(Shipped)
    sm.TransitionTo(Completed)

    fmt.Printf("最终状态: %s, 库存: %d, 已通知: %v\n", o.State, o.Stock, o.Notified)
}

运行与验证

运行方式:

go run order_state_machine.go

预期输出(由代码逻辑唯一确定):

初始状态: 待支付, 库存: 5
[清理] 订单 A1001 离开待支付,取消未支付定时器
[库存] 订单 A1001 支付成功,扣减库存,剩余 4
[通知] 订单 A1001 已支付,通知用户
[清理] 订单 A1001 离开已支付,关闭支付确认页
[通知] 订单 A1001 已发货,通知用户
最终状态: 已完成, 库存: 4, 已通知: true

验证判定标准:

  • 输出中“清理”动作发生在状态变更前,“通知”动作发生在状态变更后,顺序符合进入/退出语义;
  • 库存从5减至4,仅扣减一次,证明转换动作只执行一次;
  • 最终状态为“已完成”,且“已通知”为true,证明动作副作用已正确写入订单对象。

关键设计说明

  • 动作注册采用三个独立map,分别挂载进入、退出和转换动作,新增状态或动作时无需修改转换逻辑;
  • 转换动作以[旧状态, 新状态]为键,支持针对特定路径的定制逻辑;
  • 执行顺序固定为:退出 → 转换 → 更新状态 → 进入,保证动作与状态一致性;
  • 若某状态未注册动作,map查找返回false,自动跳过,不影响转换流程。
待支付 已支付 已发货 已完成 退出: 取消定时器 转换: 扣库存 进入: 发送通知 退出: 关闭确认页 进入: 发货通知 无动作 动作执行顺序(每次转换): 1. 退出动作(旧状态) → 2. 转换动作(路径专属) → 3. 更新状态字段 → 4. 进入动作(新状态)