Menu
CoddyTech

状态模式

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 95 课,共 107 课。

状态模式允许对象在内部状态发生变化时改变其行为,让对象看起来像是改变了类。模板方法模式控制算法步骤,而状态模式将特定于状态的行为封装到不同的对象中,并委托给当前状态。

在 Go 中,我们定义一个状态接口,并创建实现不同行为的具体状态:

type State interface {
    Handle(d *Document) string
}

type Document struct {
    state State
}

func (d *Document) SetState(s State) {
    d.state = s
}

func (d *Document) Publish() string {
    return d.state.Handle(d)
}

每种状态都会决定接下来发生什么,以及下一个状态是什么:

type DraftState struct{}

func (s DraftState) Handle(d *Document) string {
    d.SetState(ModerationState{})
    return "Draft submitted for moderation"
}

type ModerationState struct{}

func (s ModerationState) Handle(d *Document) string {
    d.SetState(PublishedState{})
    return "Moderation approved, now published"
}

type PublishedState struct{}

func (s PublishedState) Handle(d *Document) string {
    return "Already published"
}

同一个方法调用会根据当前状态产生不同的结果:

doc := &Document{state: DraftState{}}

fmt.Println(doc.Publish())  // 草稿已提交审核
fmt.Println(doc.Publish())  // 审核通过,现已发布
fmt.Println(doc.Publish())  // 已经发布

状态非常适合具有不同运行模式的对象,例如订单处理工作流、UI 组件或连接处理程序。这些对象的行为完全取决于当前状态。

challenge icon

挑战

简单

让我们使用状态模式来构建一个工单支持系统!你将创建一个支持工单,让它依次经历不同阶段,从已开启、处理中到已解决。每个状态都会决定可以执行哪些操作,以及接下来会发生什么。

你需要将代码分成三个文件:

  • state.go:定义状态接口,以及代表工单生命周期各阶段的具体状态。

    创建一个 TicketState 接口,其中包含方法 Handle(t *Ticket) string,用于处理工单,并可能将其转换到下一个状态。

    实现三个状态:

    • OpenState:处理时,将工单转换到 InProgressState,并返回 Ticket opened, assigning to support team
    • InProgressState:处理时,将工单转换到 ResolvedState,并返回 Working on ticket, issue resolved
    • ResolvedState:处理时,保持当前状态,并返回 Ticket already resolved
  • ticket.go:创建工单结构体,用于保存当前状态并将行为委托给该状态。

    创建一个 Ticket 结构体,包含 ID 字段(string 类型)和 state 字段(TicketState 类型)。添加以下方法:

    • SetState(s TicketState):用于更改工单的当前状态
    • Process() string:将处理委托给当前状态的 Handle 方法

    创建一个 NewTicket(id string) *Ticket 构造函数,返回初始状态为 OpenState 的工单。

  • main.go:演示同一操作如何根据工单的当前状态产生不同结果。

    读取工单 ID 和处理工单的次数。使用该 ID 创建新工单,然后按指定次数调用 Process(),并将每个结果分别打印在单独一行。

输入格式如下:

  • 第 1 行:工单 ID
  • 第 2 行:处理工单的次数

例如,给定:

TKT-001
3

输出应为:

Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolved

再例如,给定:

TKT-500
5

输出应为:

Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolved
Ticket already resolved
Ticket already resolved

再例如,给定:

ISSUE-42
1

输出应为:

Ticket opened, assigning to support team

注意,对同一工单调用 Process() 会在每次调用时产生不同结果:工单在不同状态间转换时,其行为也会随之改变。工单解决后,无论你处理多少次,它都会保持已解决状态。工单对象看起来像是改变了行为,但实际上,它把操作委托给了不同的状态对象!

亲手试试

package main

import (
	"fmt"
)

func main() {
	// 读取输入
	var ticketID string
	var numProcesses int
	fmt.Scanln(&ticketID)
	fmt.Scanln(&numProcesses)

	// TODO: 使用给定的 ID 创建一张新工单

	// TODO: 按指定次数处理工单
	// 并将每次的结果分别打印在单独一行
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器