AIGC标识 状态模式(State Pattern)

状态模式(State Pattern)

一、模式定义

状态模式允许一个对象在其内部状态改变时改变自身的行为。对象看起来好像修改了自身的类,实际上是把不同状态下的行为委托给不同的状态对象来处理。

一句话理解:同一个方法调用,在不同状态下做完全不同的事情。 就像你的手机——来电时按音量键调铃声音量,听音乐时按音量键调媒体音量,拍照时按音量键当快门。按键没变,行为随状态变了。

二、解决什么问题

  • 状态爆炸:一个对象有多个状态,每个状态下的行为不同。如果用 if-else/switch 管理,代码会变成一个巨大的条件分支迷宫
  • 状态转换规则复杂:状态之间的跳转有严格约束(A只能到B,B能到C或D,C不能直接到A),分散在各方法中的 if-else 很难维护状态机的一致性
  • 开闭原则:新增一个状态时不应该修改已有状态代码

三、核心角色

角色 职责
State(状态接口) 声明所有状态共有的行为方法
ConcreteState 实现特定状态下的行为,负责状态转换逻辑
Context(上下文) 持有当前状态引用,将行为调用委托给状态对象

四、结构图

┌─────────────────────┐       ┌──────────────────────┐
│    Context          │       │    State              │
├─────────────────────┤       ├──────────────────────┤
│ - state State       │──────>│ + HandleRequest()    │
│ + Request()         │       │ + NextState(ctx)     │
│ + SetState(s)      │       └──────────┬───────────┘
│ + GetState() State │                  │ implements
└─────────────────────┘     ┌────────────┼────────────┐
                           │            │            │
                    ┌──────┴──┐  ┌──────┴──┐  ┌──────┴──┐
                    │StateA   │  │StateB   │  │StateC   │
                    │空闲态   │  │处理态   │  │完成态   │
                    │Handle() │  │Handle() │  │Handle() │
                    └─────────┘  └─────────┘  └─────────┘

五、与策略模式的本质区别

虽然结构完全一样,但状态转换的控制权不同

  • 策略模式:由外部调用者(Client)决定用哪个策略,Context 不知道策略之间的存在
  • 状态模式:由状态对象自己决定下一个状态,Context 不知道状态转换的细节

这意味着状态类需要持有 Context 的引用(或通过参数传入),才能在行为执行完毕后触发状态切换。

六、完整练习代码

自动售货机为场景:有投币态、选择商品态、出货态、退货态四个状态。

package main

import "fmt"

// ============================================================
// 前向声明:VendingMachine 结构体
// ============================================================

// VendingMachine 售货机上下文
type VendingMachine struct {
	state    VendingState
	balance  int    // 当前余额(分)
	product  string // 选择的商品
	price    int    // 商品价格(分)
	stock    int    // 库存
}

// ============================================================
// 状态接口
// ============================================================

// VendingState 售货机状态接口
type VendingState interface {
	// InsertCoin 投币
	InsertCoin(vm *VendingMachine, amount int)
	// SelectProduct 选择商品
	SelectProduct(vm *VendingMachine, product string)
	// Dispense 出货
	Dispense(vm *VendingMachine)
	// Refund 退款
	Refund(vm *VendingMachine)
	// Name 状态名称
	Name() string
}

// ============================================================
// 具体状态实现
// ============================================================

// --- 1. 空闲态(等待投币)---

type IdleState struct{}

func (s *IdleState) Name() string { return "空闲态" }

func (s *IdleState) InsertCoin(vm *VendingMachine, amount int) {
	vm.balance += amount
	fmt.Printf("  [空闲态] 投入 %d 分,当前余额 %d 分\n", amount, vm.balance)
	// 余额大于0,切换到投币态
	vm.SetState(&HasCoinState{})
}

func (s *IdleState) SelectProduct(vm *VendingMachine, product string) {
	fmt.Println("  [空闲态] 请先投币再选择商品")
}

func (s *IdleState) Dispense(vm *VendingMachine) {
	fmt.Println("  [空闲态] 请先投币")
}

func (s *IdleState) Refund(vm *VendingMachine) {
	fmt.Println("  [空闲态] 没有余额可退")
}

// --- 2. 已投币态(等待选择商品)---

type HasCoinState struct{}

func (s *HasCoinState) Name() string { return "投币态" }

func (s *HasCoinState) InsertCoin(vm *VendingMachine, amount int) {
	vm.balance += amount
	fmt.Printf("  [投币态] 再投 %d 分,当前余额 %d 分\n", amount, vm.balance)
}

func (s *HasCoinState) SelectProduct(vm *VendingMachine, product string) {
	if vm.stock <= 0 {
		fmt.Printf("  [投币态] %s 已售罄,请退款\n", product)
		return
	}
	vm.product = product
	fmt.Printf("  [投币态] 选择商品: %s(价格 %d 分)\n", product, vm.price)

	// 检查余额是否足够
	if vm.balance >= vm.price {
		// 余额足够,切换到出货态
		vm.SetState(&DispensingState{})
	} else {
		// 余额不足,提示并保持投币态
		fmt.Printf("  [投币态] 余额不足,还差 %d 分\n", vm.price-vm.balance)
	}
}

func (s *HasCoinState) Dispense(vm *VendingMachine) {
	fmt.Println("  [投币态] 请先选择商品")
}

func (s *HasCoinState) Refund(vm *VendingMachine) {
	fmt.Printf("  [投币态] 退款 %d 分\n", vm.balance)
	vm.balance = 0
	vm.SetState(&IdleState{})
}

// --- 3. 出货态(正在出货)---

type DispensingState struct{}

func (s *DispensingState) Name() string { return "出货态" }

func (s *DispensingState) InsertCoin(vm *VendingMachine, amount int) {
	fmt.Println("  [出货态] 正在出货,请勿投币")
}

func (s *DispensingState) SelectProduct(vm *VendingMachine, product string) {
	fmt.Println("  [出货态] 正在出货,无法更改商品")
}

func (s *DispensingState) Dispense(vm *VendingMachine) {
	vm.stock--
	change := vm.balance - vm.price
	fmt.Printf("  [出货态] 商品 %s 已出货\n", vm.product)
	if change > 0 {
		fmt.Printf("  [出货态] 找零 %d 分\n", change)
	}
	// 重置状态
	vm.balance = 0
	vm.product = ""

	// 如果还有库存,回到空闲态;否则进入售罄态
	if vm.stock > 0 {
		vm.SetState(&IdleState{})
	} else {
		vm.SetState(&SoldOutState{})
	}
}

func (s *DispensingState) Refund(vm *VendingMachine) {
	fmt.Println("  [出货态] 正在出货,无法退款")
}

// --- 4. 售罄态 ---

type SoldOutState struct{}

func (s *SoldOutState) Name() string { return "售罄态" }

func (s *SoldOutState) InsertCoin(vm *VendingMachine, amount int) {
	fmt.Println("  [售罄态] 商品已售罄,拒绝投币")
}

func (s *SoldOutState) SelectProduct(vm *VendingMachine, product string) {
	fmt.Println("  [售罄态] 商品已售罄")
}

func (s *SoldOutState) Dispense(vm *VendingMachine) {
	fmt.Println("  [售罄态] 无货可出")
}

func (s *SoldOutState) Refund(vm *VendingMachine) {
	if vm.balance > 0 {
		fmt.Printf("  [售罄态] 退款 %d 分\n", vm.balance)
		vm.balance = 0
	}
	vm.SetState(&IdleState{})
}

// ============================================================
// Context 方法实现
// ============================================================

func NewVendingMachine(price, stock int) *VendingMachine {
	return &VendingMachine{
		state:   &IdleState{},
		price:   price,
		stock:   stock,
		balance: 0,
	}
}

func (vm *VendingMachine) SetState(s VendingState) {
	fmt.Printf("  状态切换: %s -> %s\n", vm.state.Name(), s.Name())
	vm.state = s
}

func (vm *VendingMachine) GetState() VendingState {
	return vm.state
}

func (vm *VendingMachine) InsertCoin(amount int) {
	vm.state.InsertCoin(vm, amount)
}

func (vm *VendingMachine) SelectProduct(product string) {
	vm.state.SelectProduct(vm, product)
}

func (vm *VendingMachine) Dispense() {
	vm.state.Dispense(vm)
}

func (vm *VendingMachine) Refund() {
	vm.state.Refund(vm)
}

// ============================================================
// 主函数:演示售货机状态流转
// ============================================================

func main() {
	// 创建售货机:商品价格200分(2元),库存2件
	vm := NewVendingMachine(200, 2)

	fmt.Println("===== 场景1: 正常购买 =====")
	fmt.Println("当前状态:", vm.GetState().Name())

	vm.InsertCoin(100)        // 投入1元
	vm.InsertCoin(100)        // 再投1元
	vm.SelectProduct("可乐")  // 选择商品
	vm.Dispense()             // 出货

	fmt.Println("\n===== 场景2: 余额不足再补投 =====")
	fmt.Println("当前状态:", vm.GetState().Name())

	vm.InsertCoin(100)        // 投入1元(余额100,不够)
	vm.SelectProduct("可乐")  // 余额不足提示
	vm.InsertCoin(100)        // 再投1元(余额200,够了)
	vm.SelectProduct("可乐")  // 选择商品
	vm.Dispense()             // 出货(库存→0,进入售罄态)

	fmt.Println("\n===== 场景3: 售罄后尝试购买 =====")
	fmt.Println("当前状态:", vm.GetState().Name())

	vm.InsertCoin(200)       // 售罄态拒绝投币
	vm.SelectProduct("可乐") // 售罄态拒绝选择
	vm.Refund()              // 无余额可退,回到空闲态
	fmt.Println("最终状态:", vm.GetState().Name())
}

七、运行结果

===== 场景1: 正常购买 =====
当前状态: 空闲态
  [空闲态] 投入 100 分,当前余额 100 分
  状态切换: 空闲态 -> 投币态
  [投币态] 再投 100 分,当前余额 200 分
  [投币态] 选择商品: 可乐(价格 200 分)
  状态切换: 投币态 -> 出货态
  [出货态] 商品 可乐 已出货
  状态切换: 出货态 -> 空闲态

===== 场景2: 余额不足再补投 =====
当前状态: 空闲态
  [空闲态] 投入 100 分,当前余额 100 分
  状态切换: 空闲态 -> 投币态
  [投币态] 选择商品: 可乐(价格 200 分)
  [投币态] 余额不足,还差 100 分
  [投币态] 再投 100 分,当前余额 200 分
  [投币态] 选择商品: 可乐(价格 200 分)
  状态切换: 投币态 -> 出货态
  [出货态] 商品 可乐 已出货
  状态切换: 出货态 -> 售罄态

===== 场景3: 售罄后尝试购买 =====
当前状态: 售罄态
  [售罄态] 商品已售罄,拒绝投币
  [售罄态] 商品已售罄
  [售罄态] 没有余额可退
  状态切换: 售罄态 -> 空闲态
最终状态: 空闲态

三个场景串联了完整的状态流转链:空闲态 → 投币态 → 出货态 → 空闲态/售罄态 → 空闲态。

八、关键设计决策解析

1. 状态对象是否需要持有 Context 引用

在本实现中,状态对象通过方法参数接收 *VendingMachine,而不是在构造时保存引用。好处是状态对象可以是无状态的(IdleStateHasCoinState 等都是零字段结构体),可以复用单例实例。

如果状态对象需要维护自己的数据(比如记录进入该状态的时间),则可以在构造时传入 Context 引用。

2. 状态转换放在哪里

两种风格:

  • 状态类内部转换(本实现):状态类在 Handle 方法中调用 vm.SetState() 切换状态。优点是状态转换规则集中在一处,易于理解状态机
  • Context 中集中转换:Context 在调用 state.Handle() 后根据返回值决定下一个状态。优点是状态类更纯粹,缺点是 Context 变胖

实际项目中,简单状态机用第一种更直观;复杂状态机建议用状态转换表(状态+事件→下一状态的映射表)。

3. 用 map 实现状态转换表

当状态和事件多到一定程度时,可以用矩阵式转换表替代每个状态类中的 if-else:

// transitionTable[当前状态][事件] = 下一个状态
var transitionTable = map[string]map[string]string{
	"空闲态": {"投币": "投币态", "选择": "空闲态", "出货": "空闲态", "退款": "空闲态"},
	"投币态": {"投币": "投币态", "选择足够": "出货态", "选择不足": "投币态", "出货": "投币态", "退款": "空闲态"},
	// ...
}

这种方式把转换规则从代码中抽离到数据结构中,新增状态只需改表不改代码,真正实现开闭原则。

九、实际项目身影

  • TCP 连接状态机:LISTEN → SYN_SENT → ESTABLISHED → FIN_WAIT → CLOSED,每个状态下对报文的处理完全不同
  • HTTP 请求生命周期:Idle → Reading → Processing → Writing → Closed
  • 游戏角色状态:站立/跑/跳/攻击/受伤,同一输入在不同状态下触发不同动作
  • Go 的 database/sql 连接池:连接有 free/in-use/expired 等状态,不同状态下获取/归还行为不同

十、小结

状态模式的核心价值是把状态驱动的条件分支转化为多态分发。当你发现一个对象有大量 if state == X { ... } else if state == Y { ... } 的代码,而且每个分支都很长时,就是状态模式上场的信号。Go 中通过接口+结构体的组合,可以非常优雅地实现状态机,且状态对象可以设计为零字段的轻量单例。

posted @ 2026-08-04 09:28  FfHUCisI  阅读(5)  评论(0)    收藏  举报