设计模式-状态模式

1. 简介

当对象可能会根据不同的情况做出不同的行为,我们就把这种对象称为有状态的对象,而把影响对象行为的一个或多个动态变化的属性称为状态。

当有状态的对象与外部事件产生互动时,其内部状态就会发生改变,从而使其行为也发生改变。

对这种有状态的对象编程有两种解决方法:

传统的解决方案
使用 if-else 或 switch-case 语句来做状态判断,再进行不同情况的处理。
但是显然这种做法存在弊端:
条件判断语句会过于臃肿,可读性差,且不具备扩展性,维护难度也大。且增加新的状态时要添加新的 if-else 语句,这违背了“开闭原则”,不利于程序的扩展。

状态模式的解决方案:
当控制一个对象状态转换的条件表达式过于复杂时,把相关“判断逻辑”提取出来,用各个不同的类进行表示,系统处于哪种情况,直接使用相应的状态类对象进行处理。
这样能把原来复杂的逻辑判断简单化,消除了 if-else、switch-case 等冗余语句,代码更有层次性,并且具备良好的扩展力。

状态模式
允许对象在内部状态改变时改变它的行为,对象看起来好像被修改了它的类

3. 适用场景

通常在以下情况下可以考虑使用状态模式。

  • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
  • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。

Unity中的动画控制器本质上来讲就是个使用状态模式的有限状态机

4. 模式结构

状态模式包含以下主要角色。

  • 环境类(Context)角色:
    也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
  • 抽象状态(State)角色:
    定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
  • 具体状态(Concrete State)角色:
    实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。

类图如下图所示:
image-20210831211640607

5. 状态机

为了能够更好的描述状态模式中各种状态之间的转换关系,我们一般会使用有限状态机来更加具体的描述它。

5.1 什么是状态机

定义:
有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。

要素:

  • 现态:
    是指当前所处的状态。
  • 条件:
    又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
  • 动作:
    条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
  • 次态:
    条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。

5.2 如何实现状态机

实现状态机主要体现在两部:
画出状态转换图和将有限状态机用代码实现。
其中代码实现部分其实就是状态模式的实现。

  1. 画出状态转换图

    1. 找出所有状态(圆圈表示)
    2. 找出所有状态间的转换条件(圆圈间的线段表示)
    3. 分析每个状态需要执行的策略(圆圈边的大括号表示)
  2. 将有限状态机用代码实现

    1. 定义一个状态机类
    2. 根据状态转换图的状态节点(圆圈)**,定义状态**(可使用类(推荐),枚举或无符号整数)
    3. 根据状态转换图的转换条件(边)**,实现转换动作**方法

6. 模式案例

6.1 案例分析

案例描述:
用状态模式设计一个多线程的状态转换程序

状态分析:
多线程存在 5 种状态,分别为:

  • 新建状态(New)
  • 就绪状态(Runnable )
  • 运行状态(Running)
  • 阻塞状态(Blocked)
  • 死亡状态(Dead)

各个状态当遇到相关方法调用或事件触发时会转换到其他状态。

状态转换图:
image-20210831223512143

6.2 类定义

image-20210831223846969

6.3 代码实现

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
public class ScoreStateTest {
public static void main(String[] args) {
ThreadContext context = new ThreadContext();
context.start();
context.getCPU();
context.suspend();
context.resume();
context.getCPU();
context.stop();
}
}
//环境类
class ThreadContext {
private ThreadState state;
ThreadContext() {
state = new New();
}
public void setState(ThreadState state) {
this.state = state;
}
public ThreadState getState() {
return state;
}
public void start() {
((New) state).start(this);
}
public void getCPU() {
((Runnable) state).getCPU(this);
}
public void suspend() {
((Running) state).suspend(this);
}
public void stop() {
((Running) state).stop(this);
}
public void resume() {
((Blocked) state).resume(this);
}
}
//抽象状态类:线程状态
abstract class ThreadState {
protected String stateName; //状态名
}
//具体状态类:新建状态
class New extends ThreadState {
public New() {
stateName = "新建状态";
System.out.println("当前线程处于:新建状态.");
}
public void start(ThreadContext hj) {
System.out.print("调用start()方法-->");
if (stateName.equals("新建状态")) {
hj.setState(new Runnable());
} else {
System.out.println("当前线程不是新建状态,不能调用start()方法.");
}
}
}
//具体状态类:就绪状态
class Runnable extends ThreadState {
public Runnable() {
stateName = "就绪状态";
System.out.println("当前线程处于:就绪状态.");
}
public void getCPU(ThreadContext hj) {
System.out.print("获得CPU时间-->");
if (stateName.equals("就绪状态")) {
hj.setState(new Running());
} else {
System.out.println("当前线程不是就绪状态,不能获取CPU.");
}
}
}
//具体状态类:运行状态
class Running extends ThreadState {
public Running() {
stateName = "运行状态";
System.out.println("当前线程处于:运行状态.");
}
public void suspend(ThreadContext hj) {
System.out.print("调用suspend()方法-->");
if (stateName.equals("运行状态")) {
hj.setState(new Blocked());
} else {
System.out.println("当前线程不是运行状态,不能调用suspend()方法.");
}
}
public void stop(ThreadContext hj) {
System.out.print("调用stop()方法-->");
if (stateName.equals("运行状态")) {
hj.setState(new Dead());
} else {
System.out.println("当前线程不是运行状态,不能调用stop()方法.");
}
}
}
//具体状态类:阻塞状态
class Blocked extends ThreadState {
public Blocked() {
stateName = "阻塞状态";
System.out.println("当前线程处于:阻塞状态.");
}
public void resume(ThreadContext hj) {
System.out.print("调用resume()方法-->");
if (stateName.equals("阻塞状态")) {
hj.setState(new Runnable());
} else {
System.out.println("当前线程不是阻塞状态,不能调用resume()方法.");
}
}
}
//具体状态类:死亡状态
class Dead extends ThreadState {
public Dead() {
stateName = "死亡状态";
System.out.println("当前线程处于:死亡状态.");
}
}

程序运行结果如下:

1
2
3
4
5
6
7
当前线程处于:新建状态.
调用start()方法-->当前线程处于:就绪状态.
获得CPU时间-->当前线程处于:运行状态.
调用suspend()方法-->当前线程处于:阻塞状态.
调用resume()方法-->当前线程处于:就绪状态.
获得CPU时间-->当前线程处于:运行状态.
调用stop()方法-->当前线程处于:死亡状态.

7. 总结

7.2 优缺点

优点:

  • 结构清晰:
    状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
  • 将状态转换显示化,减少对象间的相互依赖:
    将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
  • 状态类职责明确,有利于程序的扩展:
    通过定义新的子类很容易地增加新的状态和转换。

缺点:

  • 状态模式的使用必然会增加系统的类与对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
  • 状态模式对开闭原则的支持并不太好:
    对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。

7.3 与其他模式的区别

状态模式与策略模式看起来像双胞胎,但他们还是不相同的。

模式 描述
策略模式 将可以互换的行为封装,使用委托的方法决定使用哪个行为
状态莫斯 封装基于状态的行为,将行为委托至当前状态
模板方法 由子类决定如何实现算法中的某些步骤