状态模式让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。

  • 状态模式与有限状态机的概念紧密相关。
  • 主要思想是程序在任意时刻仅可处于几种有限的状态中。
  • 在任何一个特定状态中,程序的行为都不相同,且可瞬间从一个状态切换到另一个状态。
  • 根据当前状态,程序可能会切换到另外一种状态,也可能会保持当前状态不变
  • 这些数量有限且预先定义的状态切换规则被称为转移。

背景

假如有一个文档Document类。文档可能会处于草稿Draft、审阅中Moderation和已发布Published三种状态中的一种。 文档的publish发布方法在不同状态下的行为略有不同:

  • 处于 草稿状态时,它会将文档转移到审阅中状态。
  • 处于 审阅中状态时,如果当前用户是管理员,它会公开发布文档。
  • 处于 已发布状态时,它不会进行任何操作。

状态机通常由众多条件运算符 (if或switch)实现,可根据对象的当前状态选择相应的行为。

当我们逐步在文档类中添加更多状态和依赖于状态的行为后,基于条件语句的状态机就会暴露其最大的弱点。

  • 为了能根据当前状态选择完成相应行为的方法,绝大部分方法中会包含复杂的条件语句。
  • 修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句,导致代码的维护工作非常艰难。
  • 很难在设计阶段预测所有可能的状态和转换。随着时间推移,最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。

解决方案

状态模式建议为对象的每个可能状态新建一个类,然后将所有状态的对应行为抽取到这些类中。

原始对象被称为上下文 (context)

  • 不会自行实现所有行为
  • 会保存一个指向表示当前状态的状态对象的引用
  • 将所有与状态相关的工作委派给该对象。

如需将上下文转换为另外一种状态,则需将当前活动的状态对象替换为另外一个代表新状态的对象。

  • 采用这种方式前提是:所有状态类都必须遵循同样的接口,而且上下文必须仅通过接口与这些对象进行交互。

这个结构看上去与策略模式相似,但有一个关键性的不同

  • 在状态模式中,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换
  • 策略则几乎完全不知道其他策略的存在

状态模式

应用场景

  • 如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式。
    • 建议将所有特定于状态的代码抽取到独立的状态类中。这样可以在独立于其他状态的情况下添加新状态或修改已有状态,从而减少维护成本。
  • 如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。
    • 状态模式会将这些条件语句的分支抽取到相应状态类的方法中
    • 还可以清除主要类中与特定状态相关的临时成员变量和helper方法代码。
  • 当相似状态和基于条件的状态机转换中存在许多重复代码时,可使用状态模式。
    • 状态模式让你能够生成状态类层次结构,通过将公用代码抽取到抽象基类中来减少重复。

实现步骤

  1. 确定哪些类是上下文。它可能是包含依赖于状态的代码的已有类;如果特定于状态的代码分散在多个类中,那么它可能是一个新的类。
  2. 声明状态接口,把关注点放在那些可能包含特定于状态的行为的方法上。
  3. 为每个实际状态创建一个继承于状态接口的类。然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。
    • 在将代码移动到状态类的过程中,可能会发现它依赖于上下文中的一些私有成员。可以采用以下几种变通方式:
      • 将这些成员变量或方法设为公有。
      • 将需要抽取的上下文行为更改为上下文中的公有方法,然后在状态类中调用。
      • 将状态类嵌套在上下文类中。这种方式需要使用的编程语言支持嵌套类。
  4. 在上下文类中添加一个状态接口类型的引用成员变量,以及一个用于修改该成员变量值的公有设置器。
  5. 再次检查上下文中的方法,将空的条件语句替换为相应的状态对象方法。
  6. 为切换上下文状态,需要创建某个状态类实例并将其传递给上下文。可以在上下文、各种状态或客户端中完成这项工作。
  • 会在这几个位置引入对具体状态类的依赖。

优缺点

优点

  • 单一职责原则。将与特定状态相关的代码放在单独的类中。
  • 开闭原则。无需修改已有状态类和上下文就能引入新状态。
  • 通过消除臃肿的条件语句简化上下文代码。

缺点 如果状态机只有很少的几个状态,或者很少发生改变,那么应用该模式可能会显得小题大作。

与其它模式的关系

  • 桥接模式状态模式策略模式(在某种程度上包括适配器模式)模式的接口非常相似。
    • 实际上,它们都基于组合模式——将工作委派给其他对象。不过也各自解决了不同的问题。
    • 模式并不只是以特定方式组织代码的配方,还可以使用它们来和其他开发者讨论模式所解决的问题。
  • 状态可被视为策略的扩展。两者都基于组合机制:它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。
    • 策略使得这些对象相互之间完全独立,它们不知道其他对象的存在。
    • 状态模式没有限制具体状态之间的依赖,且允许它们自行改变在不同情景下的状态。

示例

设计一台自动售货机。 为简单起见,假设自动售货机仅销售一种类型的商品。

假设自动售货机可处于 4 种不同的状态:

  • 有商品 (hasItem)
  • 无商品 (noItem)
  • 商品已请求 (itemRequested)
  • 收到纸币 (hasMoney)

假设其只会执行 4 种操作:

  • 选择商品
  • 添加商品
  • 插入纸币
  • 提供商品

当对象可以处于许多不同的状态中时应使用状态设计模式,同时根据传入请求的不同,对象需要变更其当前状态。

在我们的例子中,自动售货机可以有多种不同的状态,同时会在这些状态之间持续不断地互相转换。

我们假设自动售货机处于 商品已请求状态中。在 “插入纸币” 的操作发生后,机器将自动转换至收到纸币状态。

根据其当前状态,机器可就相同请求采取不同的行为。例如,如果用户想要购买一件商品,机器将在 有商品状态时继续操作,而在 无商品状态时拒绝操作。

所有依赖于状态的代码都存在于各自的状态实现中。

state.go

1
2
3
4
5
6
7
8
9
package main

type state interface {
	addItem(int) error
	requestItem() error
	insertMoney(money int) error
	dispenseItem() error
}

hasItemState.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

type hasItemState struct {
	vendingMachine *vendingMachine
}

func (i *hasItemState) requestItem() error {
	if i.vendingMachine.itemCount == 0 {
		i.vendingMachine.setState(i.vendingMachine.noItem)
		return fmt.Errorf("No item present\n")
	}
	fmt.Printf("Item requestd\n")
	i.vendingMachine.setState(i.vendingMachine.itemRequested)
	return nil
}

func (i *hasItemState) addItem(count int) error {
	fmt.Printf("%d items added\n", count)
	i.vendingMachine.incrementItemCount(count)
	return nil
}

func (i *hasItemState) insertMoney(money int) error {
	return fmt.Errorf("Please select item first\n")
}
func (i *hasItemState) dispenseItem() error {
	return fmt.Errorf("Please select item first\n")
}

itemRequestedState.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

type itemRequestedState struct {
	vendingMachine *vendingMachine
}

func (i *itemRequestedState) requestItem() error {
	return fmt.Errorf("Item already requested\n")
}

func (i *itemRequestedState) addItem(count int) error {
	return fmt.Errorf("Item Dispense in progress\n")
}

func (i *itemRequestedState) insertMoney(money int) error {
	if money < i.vendingMachine.itemPrice {
		fmt.Errorf("Inserted money is less. Please insert %d\n", i.vendingMachine.itemPrice)
	}
	fmt.Println("Money entered is ok")
	i.vendingMachine.setState(i.vendingMachine.hasMoney)
	return nil
}
func (i *itemRequestedState) dispenseItem() error {
	return fmt.Errorf("Please insert money first\n")
}

hasMoneyState.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

type hasMoneyState struct {
	vendingMachine *vendingMachine
}

func (i *hasMoneyState) requestItem() error {
	return fmt.Errorf("Item dispense in progress\n")
}

func (i *hasMoneyState) addItem(count int) error {
	return fmt.Errorf("Item dispense in progress\n")
}

func (i *hasMoneyState) insertMoney(money int) error {
	return fmt.Errorf("Item out of stock\n")
}
func (i *hasMoneyState) dispenseItem() error {
	fmt.Println("Dispensing Item")
	i.vendingMachine.itemCount = i.vendingMachine.itemCount - 1
	if i.vendingMachine.itemCount == 0 {
		i.vendingMachine.setState(i.vendingMachine.noItem)
	} else {
		i.vendingMachine.setState(i.vendingMachine.hasItem)
	}
	return nil
}

noItemState.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

type noItemState struct {
	vendingMachine *vendingMachine
}

func (i *noItemState) requestItem() error {
	return fmt.Errorf("Item out of stock\n")
}

func (i *noItemState) addItem(count int) error {
	i.vendingMachine.incrementItemCount(count)
	i.vendingMachine.setState(i.vendingMachine.hasItem)
	return nil
}

func (i *noItemState) insertMoney(money int) error {
	return fmt.Errorf("Item out of stock\n")
}
func (i *noItemState) dispenseItem() error {
	return fmt.Errorf("Item out of stock\n")
}

vendingMachine.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import "fmt"

type vendingMachine struct {
	hasItem       state
	itemRequested state
	hasMoney      state
	noItem        state

	currentState state

	itemCount int
	itemPrice int
}

func newVendingMachine(itemCount, itemPrice int) *vendingMachine {
	v := &vendingMachine{
		itemCount: itemCount,
		itemPrice: itemPrice,
	}
	hasItemState := &hasItemState{
		vendingMachine: v,
	}
	itemRequestedState := &itemRequestedState{
		vendingMachine: v,
	}
	hasMoneyState := &hasMoneyState{
		vendingMachine: v,
	}
	noItemState := &noItemState{
		vendingMachine: v,
	}

	v.setState(hasItemState)
	v.hasItem = hasItemState
	v.itemRequested = itemRequestedState
	v.hasMoney = hasMoneyState
	v.noItem = noItemState
	return v
}

func (v *vendingMachine) requestItem() error {
	return v.currentState.requestItem()
}

func (v *vendingMachine) addItem(count int) error {
	return v.currentState.addItem(count)
}

func (v *vendingMachine) insertMoney(money int) error {
	return v.currentState.insertMoney(money)
}

func (v *vendingMachine) dispenseItem() error {
	return v.currentState.dispenseItem()
}

func (v *vendingMachine) setState(s state) {
	v.currentState = s
}

func (v *vendingMachine) incrementItemCount(count int) {
	fmt.Printf("Adding %d items\n", count)
	v.itemCount = v.itemCount + count
}

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

func main() {
	m := newVendingMachine(1, 10)

	_ = m.requestItem()

	_ = m.insertMoney(10)

	_ = m.dispenseItem()

	_ = m.addItem(2)

	_ = m.requestItem()

	_ = m.insertMoney(10)

	_ = m.dispenseItem()
}

References

guru