状态模式

设计模式笔记:状态模式 (State Pattern)

一、一句话概括

允许一个对象在其内部状态改变时改变它的行为,这个对象看起来就像是改变了它的类。

二、为什么需要它 (Why - The Pain Point)

想象一下,你正在为一个糖果售卖机编写控制程序。一个售卖机有多种状态:没投币(NoCoinState)有投币(HasCoinState)糖果售罄(SoldOutState)售出糖果(SoldState)

对于用户的每个动作(insertCoin(), turnCrank()),售卖机的响应完全取决于它当前处于哪个状态。

糟糕的设计(使用条件判断):

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
public class GumballMachine {
final static int SOLD_OUT = 0;
final static int NO_COIN = 1;
final static int HAS_COIN = 2;
final static int SOLD = 3;

int state = SOLD_OUT;
int count = 0;
// ...

public void insertCoin() {
// 痛点:基于状态的巨大 if-else 或 switch 语句
if (state == HAS_COIN) {
System.out.println("You can't insert another coin");
} else if (state == NO_COIN) {
state = HAS_COIN;
System.out.println("You inserted a coin");
} else if (state == SOLD_OUT) {
System.out.println("You can't insert a coin, the machine is sold out");
} else if (state == SOLD) {
System.out.println("Please wait, we're already giving you a gumball");
}
}
// ... turnCrank() 和其他方法也充满了这样的条件判断 ...
}

这种设计的弊端非常明显:

  1. 代码难以阅读和维护:大量的条件判断语句交织在一起,逻辑混乱,难以理解。
  2. 违反开闭原则:如果需要增加一个新的状态(例如“维护中状态”),你必须修改所有方法,在每个 if-else 结构中添加新的分支。这使得代码非常脆弱。
  3. 状态转换逻辑分散:状态转换的逻辑(state = HAS_COIN;)散落在各个方法的条件分支中,而不是集中管理。

核心问题:如何优雅地管理一个拥有复杂状态流的对象,避免使用庞大的条件语句,并使状态的扩展变得容易?

三、它是什么 (What - The Solution)

状态模式提出了一种绝佳的解决方案:将每一种状态的行为封装到各自独立的类中

它让一个“上下文”对象(Context,即糖果机)持有一个对“状态”对象(State)的引用。当请求到达时,上下文对象将请求委托给当前的状态对象来处理。状态对象不仅负责执行动作,还负责决定在动作完成后,上下文应该转换到哪个新状态。

核心思想和角色:

  1. **上下文 (Context)**:
    • 就是那个拥有状态的对象,例如 GumballMachine
    • 它维护一个对当前状态对象(ConcreteState)的引用。
    • 它将所有与状态相关的请求都委托给当前的状态对象。
    • 它提供一个 setter 方法,供状态对象在需要时改变上下文的当前状态。
  2. **状态接口 (State)**:
    • 定义了一个接口,用来封装与上下文特定状态相关的行为。
    • 接口中通常包含对应于用户操作的方法,如 insertCoin(), turnCrank()
  3. **具体状态 (Concrete State)**:
    • 实现了 State 接口的类,每一个类代表一种具体的状态。
    • 它实现了在该状态下,对于各种用户操作应有的行为。
    • 它负责在适当的时候,将上下文对象的状态切换到下一个状态。

状态模式 vs. 策略模式
这两个模式的UML图几乎一模一样,但意图完全不同:

  • 状态模式:关注的是对象在不同状态下的行为变化自动的状态转移。状态的改变通常由状态对象自身或上下文内部驱动。
  • 策略模式:关注的是为对象提供可互换的算法,而这些算法的选择通常由客户端在外部决定。上下文本身不关心状态转移。

四、怎么做 (How - The Blueprint)

基本步骤(以糖果机为例):

  1. 创建 State 接口,定义所有可能的操作:insertCoin(), ejectCoin(), turnCrank(), dispense()
  2. 为每个状态创建一个具体状态类(NoCoinState, HasCoinState 等),实现 State 接口。每个类都需要持有对 GumballMachine(Context)的引用,以便能够改变其状态。
  3. 创建 GumballMachine(Context)类。
    • 持有所有可能的状态对象的实例。
    • 持有一个 currentState 变量,引用当前的状态。
    • 将所有操作(insertCoin() 等)委托给 currentState 的同名方法。
    • 提供一个 setState(State state) 方法。
  4. GumballMachine 的构造函数中,初始化所有状态对象,并设置初始状态。

Java 示例代码

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// 1. 状态接口 (State)
interface State {
void insertCoin();
void ejectCoin();
void turnCrank();
void dispense();
}

// --- 3. 上下文 (Context) ---
class GumballMachine {
// 持有所有状态的实例
private State soldOutState;
private State noCoinState;
private State hasCoinState;
private State soldState;

private State currentState;
private int count = 0; // 糖果数量

public GumballMachine(int numberGumballs) {
soldOutState = new SoldOutState(this);
noCoinState = new NoCoinState(this);
hasCoinState = new HasCoinState(this);
soldState = new SoldState(this);

this.count = numberGumballs;
if (numberGumballs > 0) {
currentState = noCoinState; // 初始状态
} else {
currentState = soldOutState;
}
}

// 动作被委托给当前状态
public void insertCoin() { currentState.insertCoin(); }
public void ejectCoin() { currentState.ejectCoin(); }
public void turnCrank() {
currentState.turnCrank();
currentState.dispense(); // 紧跟着分发
}

// Setter for state transition
void setState(State state) { this.currentState = state; }

// Getters for states to use
public State getSoldOutState() { return soldOutState; }
public State getNoCoinState() { return noCoinState; }
public State getHasCoinState() { return hasCoinState; }
public State getSoldState() { return soldState; }
public int getCount() { return count; }
public void releaseBall() {
System.out.println("A gumball comes rolling out the slot...");
if (count > 0) {
count = count - 1;
}
}
}

// --- 2. 具体状态 (Concrete State) ---
// 以 HasCoinState 为例
class HasCoinState implements State {
GumballMachine gumballMachine;

public HasCoinState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}

@Override
public void insertCoin() { System.out.println("You can't insert another coin"); }

@Override
public void ejectCoin() {
System.out.println("Coin returned");
gumballMachine.setState(gumballMachine.getNoCoinState()); // 转换到 NoCoinState
}

@Override
public void turnCrank() {
System.out.println("You turned...");
gumballMachine.setState(gumballMachine.getSoldState()); // 转换到 SoldState
}

@Override
public void dispense() { System.out.println("No gumball dispensed"); }
}

// 其他状态类 (NoCoinState, SoldState, SoldOutState) ... 篇幅所限,省略具体实现
class NoCoinState implements State {
GumballMachine machine;
public NoCoinState(GumballMachine m) { this.machine = m; }
public void insertCoin() {
System.out.println("You inserted a coin");
machine.setState(machine.getHasCoinState());
}
public void ejectCoin() { System.out.println("You haven't inserted a coin"); }
public void turnCrank() { System.out.println("You turned, but there's no coin"); }
public void dispense() { System.out.println("You need to pay first"); }
}

// ... SoldState 和 SoldOutState 的实现

public class StatePatternDemo {
public static void main(String[] args) {
GumballMachine gumballMachine = new GumballMachine(5);

System.out.println(gumballMachine);

gumballMachine.insertCoin();
gumballMachine.turnCrank();

System.out.println(gumballMachine);

gumballMachine.insertCoin();
gumballMachine.ejectCoin();
gumballMachine.turnCrank(); // this will fail

System.out.println(gumballMachine);
}
}

五、UML 类图

下面是上述例子的 PlantUML 代码。

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
@startuml
skinparam classAttributeIconSize 0
hide empty members

interface State {
+ void insertCoin()
+ void ejectCoin()
+ void turnCrank()
+ void dispense()
}

class GumballMachine {
- State currentState
- int count
+ void insertCoin()
+ void ejectCoin()
+ void turnCrank()
+ void setState(State s)
}

class NoCoinState implements State
class HasCoinState implements State
class SoldState implements State
class SoldOutState implements State

' Context has a State
GumballMachine o-- "1" State

' ConcreteStates may change the Context's state
NoCoinState ..> GumballMachine
HasCoinState ..> GumballMachine
SoldState ..> GumballMachine
SoldOutState ..> GumballMachine

@enduml

六、优缺点分析

优点 (Pros):

  1. 将状态相关的代码局部化:将与特定状态相关的代码都放在一个类中,使得代码更加内聚,易于理解和维护。
  2. 轻松增加新状态:增加一个新的状态只需添加一个新的具体状态类,符合开闭原则。
  3. 消除庞大的条件分支:用多态性代替了 if-elseswitch,使得代码更简洁,更面向对象。
  4. 状态转换更明确:状态的转换逻辑在具体状态类中清晰地表达出来,而不是散落在各处。

缺点 (Cons):

  1. 类数量增多:状态模式会导致系统中类的数量增加,如果状态很简单或者很少,可能会有点“小题大做”。

七、在 Java 中的应用

状态模式在需要对对象生命周期进行精细管理的场景中非常有用。

  • **工作流引擎 (Workflow Engine)**:一个工单(Ticket)对象可以在“待处理”、“处理中”、“已解决”、“已关闭”等状态之间流转。每个状态下,允许的操作(如“分配”、“解决”)都不同。
  • 网络连接管理:一个 Connection 对象可以有“正在连接”、“已连接”、“已断开”、“超时”等状态,每个状态下对 send()receive() 的响应都不同。
  • 游戏开发:游戏角色的状态机(站立、行走、跑步、跳跃、攻击),使用状态模式可以清晰地管理角色行为和动画的切换。