3. 状态机中的守卫条件

状态机中的守卫条件 学习卡片

任务说明

在订单状态机中,当用户尝试从“待支付”转换到“已支付”时,需要校验库存是否充足。若库存不足,转换必须被拒绝,状态保持为“待支付”,并返回明确的失败原因。本案例实现一个带守卫条件的订单状态机核心逻辑。

核心实现

// order_state_machine.go
package main

import (
    "errors"
    "fmt"
)

// 订单状态
type OrderState int

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

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

// 订单实体
type Order struct {
    ID          string
    State       OrderState
    ProductStock int // 商品库存(简化:每个订单关联一个商品库存)
}

// 状态转换守卫条件
type Guard func(order *Order) error

// 转换定义:from -> to 及其守卫
type Transition struct {
    From   OrderState
    To     OrderState
    Guards []Guard
}

// 状态机
type StateMachine struct {
    Transitions []Transition
}

// 注册转换
func (sm *StateMachine) AddTransition(from, to OrderState, guards ...Guard) {
    sm.Transitions = append(sm.Transitions, Transition{
        From: from, To: to, Guards: guards,
    })
}

// 执行转换
func (sm *StateMachine) TransitionTo(order *Order, target OrderState) error {
    // 查找匹配的转换规则
    var matched *Transition
    for i := range sm.Transitions {
        t := &sm.Transitions[i]
        if t.From == order.State && t.To == target {
            matched = t
            break
        }
    }
    if matched == nil {
        return fmt.Errorf("非法转换: %s -> %s", order.State, target)
    }

    // 执行所有守卫条件
    for _, guard := range matched.Guards {
        if err := guard(order); err != nil {
            return err // 守卫失败,拒绝转换
        }
    }

    // 守卫全部通过,执行转换
    order.State = target
    return nil
}

// 守卫:库存充足检查
func StockSufficientGuard(order *Order) error {
    if order.ProductStock <= 0 {
        return errors.New("库存不足,无法支付")
    }
    return nil
}

func main() {
    // 构建状态机
    sm := &StateMachine{}
    // 待支付 -> 已支付(需要库存守卫)
    sm.AddTransition(PendingPayment, Paid, StockSufficientGuard)
    // 已支付 -> 已发货
    sm.AddTransition(Paid, Shipped)
    // 已发货 -> 已完成
    sm.AddTransition(Shipped, Completed)
    // 待支付 -> 已取消
    sm.AddTransition(PendingPayment, Cancelled)

    // 场景1:库存充足,支付成功
    order1 := &Order{ID: "A001", State: PendingPayment, ProductStock: 5}
    err := sm.TransitionTo(order1, Paid)
    if err != nil {
        fmt.Printf("订单%s支付失败: %v\n", order1.ID, err)
    } else {
        fmt.Printf("订单%s支付成功,当前状态: %s\n", order1.ID, order1.State)
    }

    // 场景2:库存不足,支付被拒绝
    order2 := &Order{ID: "B002", State: PendingPayment, ProductStock: 0}
    err = sm.TransitionTo(order2, Paid)
    if err != nil {
        fmt.Printf("订单%s支付失败: %v(状态保持: %s)\n", order2.ID, err, order2.State)
    } else {
        fmt.Printf("订单%s支付成功,当前状态: %s\n", order2.ID, order2.State)
    }
}

运行与验证

  1. 保存代码为 order_state_machine.go,在终端执行:
go run order_state_machine.go
  1. 预期输出(与代码逻辑严格对应):
订单A001支付成功,当前状态: 已支付
订单B002支付失败: 库存不足,无法支付(状态保持: 待支付)

判定通过条件:order1 状态变为“已支付”;order2 状态仍为“待支付”,且返回错误信息包含“库存不足”。

关键设计说明

  • 守卫条件以函数切片形式挂载在转换规则上,新增守卫只需追加函数,无需修改状态机核心逻辑。
  • 守卫返回 error 即中断转换,状态保持不变;返回 nil 则继续执行后续守卫,全部通过后完成状态迁移。
  • 非法转换(未注册的 from→to 组合)会被直接拒绝,保证状态流转受控。
待支付 已支付 已发货 已完成 已取消 守卫: 库存>0 守卫失败 → 回退

图中红色虚线表示当“库存不足”守卫失败时,转换被拒绝,订单状态回退并保持在“待支付”,不会进入“已支付”状态。