如何优雅代码中处理复杂的状态流转?go FSM 实践

159 阅读6分钟

在日常的软件开发中,我们经常会遇到需要管理对象状态或处理复杂业务流程的场景。比如订单状态(待支付、已支付、已发货、已完成、已取消)、任务状态(待分配、处理中、已完成、已驳回)等等。如果状态和转换逻辑比较复杂,直接用 if-elseswitch-case 来堆砌代码,很容易写出难以理解和维护的 "Spaghetti Code"(意大利面条代码)。

今天,我们就来聊聊一种优雅处理这类问题的设计模式——有限状态机 (Finite State Machine, FSM) ,并结合一个 Go 语言的实例,看看如何在实践中应用它。

恼人的状态管理难题

想象一下,我们正在开发一个任务管理系统。一个任务从创建到最终完成或取消,可能会经历多个状态:

  • 待派发 (Pending)
  • 已派发 (Assigned)
  • 已接单 (Accepted)
  • 已提交 (Submitted)
  • 已审核 (Approved)
  • 已驳回 (Rejected)
  • 已结算 (Settled)
  • 已取消 (Canceled)

这些状态之间还存在特定的转换规则:

  • pending 状态的任务可以被 assign(派发)或 cancel(取消)。
  • assigned 状态的任务可以被 accept(接受)或 cancel(取消)。
  • accepted 状态的任务可以被 submit(提交)。
  • ... 等等

如果不用 FSM,我们可能会在处理任务操作的代码里写大量的 if-else 来判断当前状态和允许的操作,代码会变得臃肿且难以维护。每次新增状态或修改转换逻辑,都可能牵一发而动全身。

FSM 登场:让状态管理井然有序

有限状态机 (FSM) 提供了一种清晰的模型来描述对象在不同状态之间的转换。它的核心要素包括:

  • 状态 (State): 对象可能存在的不同情况。
  • 事件 (Event): 触发状态转换的动作或信号。
  • 转换 (Transition): 在特定状态下,由某个事件触发,导致状态变更到另一个状态的过程。
  • 动作/回调 (Action/Callback): 在状态进入、退出或转换过程中执行的逻辑。

在 Go 语言中,有一个好用的 FSM 库 looplab/fsm (github.com/looplab/fsm…

Go 实战:构建任务状态机

接下来,我们看看如何使用 looplab/fsm 来实现前面提到的任务管理状态机。

1. 定义状态和事件常量:

首先,我们用常量清晰地定义所有可能的状态和事件。

// 定义任务状态
const (
	StatePending   = "pending"   // 待派发
	StateAssigned  = "assigned"  // 已派发
	StateAccepted  = "accepted"  // 已接单
	StateSubmitted = "submitted" // 已提交
	StateApproved  = "approved"  // 已审核
	StateRejected  = "rejected"  // 已驳回
	StateSettled   = "settled"   // 已结算
	StateCanceled  = "canceled"  // 已取消
)

// 定义任务事件
const (
	EventAssign  = "assign"  // 派发任务
	EventAccept  = "accept"  // 接受任务
	EventSubmit  = "submit"  // 提交任务
	EventApprove = "approve" // 审核通过
	EventReject  = "reject"  // 审核驳回
	EventSettle  = "settle"  // 结算任务
	EventCancel  = "cancel"  // 取消任务
)

2. 创建任务结构体和初始化 FSM:

我们定义一个 Task 结构体,包含任务信息和一个 fsm.FSM 实例。在创建新任务时,初始化状态机。


// Task 任务结构体
type Task struct {
	ID     string
	FSM    *fsm.FSM
	UserID string // 示例:记录操作者
}

// NewTask 创建新任务
func NewTask(id string) *Task {
	task := &Task{
		ID: id,
	}

	// 创建状态机
	task.FSM = fsm.NewFSM(
		StatePending, // 初始状态
		fsm.Events{ // 定义状态转换规则
			{Name: EventAssign, Src: []string{StatePending}, Dst: StateAssigned},
			{Name: EventAccept, Src: []string{StateAssigned}, Dst: StateAccepted},
			{Name: EventSubmit, Src: []string{StateAccepted}, Dst: StateSubmitted},
			{Name: EventApprove, Src: []string{StateSubmitted}, Dst: StateApproved},
			{Name: EventReject, Src: []string{StateSubmitted}, Dst: StateRejected},
			{Name: EventSettle, Src: []string{StateApproved}, Dst: StateSettled},
			// Cancel 事件可以从 Pending 或 Assigned 状态触发
			{Name: EventCancel, Src: []string{StatePending, StateAssigned}, Dst: StateCanceled},
		},
		fsm.Callbacks{ // 定义回调函数
			// 通用回调:每次触发事件前执行(通过 e.Cancel() 可以组织内存中实际状态变化)
			"before_event": func(_ context.Context, e *fsm.Event) {
				if e.Event == EventCancel {
					e.Cancel(fmt.Errorf("failed to handle event %s", e.Event))
					return
				}
				fmt.Printf("[Event]Task %s event %s triggered\n", t.ID, e.Event)
			},
			// 通用回调:每次进入新状态时触发
			"enter_state": func(_ context.Context, e *fsm.Event) {
				fmt.Printf("[State] Task %s state changed from %s to %s", task.ID, e.Src, e.Dst)
				// 在这里可以添加通用逻辑,如更新数据库状态
			},
			// 特定状态回调:进入 Assigned 状态后执行
			StateAssigned: func(_ context.Context, e *fsm.Event) {
				fmt.Printf("[State] Task %s assigned to user %s", task.ID, task.UserID)
				// 在这里可以发送通知给被分配者
			},
		},
	)

	return task
}

NewFSM 中:

  • 第一个参数是初始状态。
  • 第二个参数 fsm.Events 定义了所有的状态转换规则:{事件名, [源状态列表], 目标状态}
  • 第三个参数 fsm.Callbacks 定义了回调函数。looplab/fsm 支持多种回调时机(如 before_<EVENT>, leave_<STATE>, enter_<STATE>, after_<EVENT> 等),可以非常灵活地在状态转换的不同阶段执行业务逻辑。

3. 触发事件:

我们为 Task 定义方法来触发状态转换事件。

// 任务状态流转方法
func (t *Task) Assign(ctx context.Context, userID string) error {
	t.UserID = userID // 可以在事件触发前设置相关信息
	return t.FSM.Event(ctx, EventAssign) // 触发 Assign 事件
}

func (t *Task) Accept(ctx context.Context) error {
	return t.FSM.Event(ctx, EventAccept)
}

func (t *Task) Submit(ctx context.Context) error {
	return t.FSM.Event(ctx, EventSubmit)
}

// ... 其他事件触发方法 ...

调用 t.FSM.Event() 方法即可尝试触发一个事件。FSM 内部会自动检查当前状态是否允许该事件发生,如果允许,则执行状态转换并触发相应的回调函数;如果不允许,则返回错误。

4. 测试: 我们在如下测试用例中验证了基本状态流转和回调报错后状态情况


func Test_FSM_NewFSM(t *testing.T) {
	ctx := context.Background()

	// 创建新任务
	fmt.Println("----------------task1----------------")
	fmt.Println("-------------验证状态流转--------------")
	task := NewTask("task-001")

	// 任务状态流转
	fmt.Println("Current state:", task.FSM.Current()) // pending

	_ = task.Assign(ctx, "user-001")
	fmt.Println("Current state:", task.FSM.Current()) // assigned

	_ = task.Accept(ctx)
	fmt.Println("Current state:", task.FSM.Current()) // accepted

	_ = task.Submit(ctx)
	fmt.Println("Current state:", task.FSM.Current()) // submitted

	_ = task.Approve(ctx)
	fmt.Println("Current state:", task.FSM.Current()) // approved

	_ = task.Settle(ctx)
	fmt.Println("Current state:", task.FSM.Current()) // settled

	// 回调报错情况
	fmt.Println("----------------task2----------------")
	fmt.Println("-------------验证回调报错情况-------------")
	task2 := NewTask("task-002")
	fmt.Println("Current state:", task2.FSM.Current()) // pending

	_ = task2.Assign(ctx, "user-002")
	fmt.Println("Current state:", task2.FSM.Current()) // assigned

	e := task2.Cancel(ctx)
	fmt.Println("Error:", e)
	fmt.Println("Current state:", task2.FSM.Current()) // assigned(cancel failed)
}

可视化你的状态机

使用 FSM 的一个巨大优势是状态转换逻辑非常清晰,并且可以轻松地可视化。我们可以通过它提供的函数来生成 Mermaid 状态图:

func TestMermaidOutput(t *testing.T) {
	task := NewTask("task-001")

	gotDiagram, err := fsm.VisualizeForMermaidWithGraphType(task.FSM, fsm.StateDiagram)
	if err != nil {
		t.Errorf("got error for visualizing with type MERMAID: %s", err)
	}

	gotFlowchart, err := fsm.VisualizeForMermaidWithGraphType(task.FSM, fsm.FlowChart)
	if err != nil {
		t.Errorf("got error for visualizing with type MERMAID: %s", err)
	}

	fmt.Println(gotDiagram)
	fmt.Println("--------------------------------")
	fmt.Println(gotFlowchart)
}

效果如下:

stateDiagram-v2
    [*] --> pending
    accepted --> submitted: submit
    approved --> settled: settle
    assigned --> accepted: accept
    assigned --> canceled: cancel
    pending --> assigned: assign
    pending --> canceled: cancel
    submitted --> approved: approve
    submitted --> rejected: reject

这张图清晰地展示了任务的所有状态以及它们之间的转换关系和触发事件。技术评审和后期维护时可以一目了然地理解整个业务流程。

FSM 的优势

总结一下,在合适的场景下使用 FSM 的优点:

  1. 逻辑清晰: 状态和转换规则集中定义,代码结构清晰,易于理解。
  2. 可维护性高: 修改或增加状态、事件、转换规则时,只需要修改 FSM 的定义,对其他业务代码影响小。
  3. 可预测性强: FSM 的行为是确定的,给定当前状态和事件,下一个状态是唯一的。
  4. 易于测试: 可以针对 FSM 的状态转换进行单元测试。
  5. 可视化: 状态转换逻辑易于通过图表展示,方便团队沟通和理解。

结语

有限状态机 (FSM) 是处理复杂状态和流程管理的强大武器。通过 looplab/fsm 这样的库,我们可以轻松地在 Go 项目中实践 FSM,告别混乱的 if-else 逻辑,写出更优雅、更健壮、更易于维护的代码。

当然,对于非常简单的状态管理,引入 FSM 可能反而会增加复杂度。但当你面对多个状态、复杂的转换规则和需要在转换过程中执行特定逻辑时,FSM 绝对值得一试!

如果这篇文章对你有所帮助,欢迎关注、点赞、分享,在评论区一起交流对复杂状态业务的处理心得~