设计模式:状态模式
场景再现
假设有一款游戏,在这个游戏中,玩家要通过操纵角色来完成任务,而角色有着各种各样的状态,在不同状态下,玩家的操作方式会有所不同,假设每个角色有下面这几个状态:
- standing(静止)
- walking(走)
- running(跑)
- jumping(跳跃)
在不同的状态下,角色的 UI 显示也会有所不同,角色在不同状态下都可以施展不同技能,另外,只有在静止的状态下才能进行交流。
于是我们可以得到下面这个这个类:
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
class Game {
private final static int STANDING = 1;
private final static int WALKING = 2;
private final static int RUNNING = 3;
private final static int JUMPING = 4;
private int state = 1;
public void display() {
if (state == STANDING) {
System.out.println("Standing...");
} else if (state == WALKING) {
System.out.println("Walking...");
} else if (state == RUNNING) {
System.out.println("Running...");
} else if (state == JUMPING) {
System.out.println("Jumping...");
}
}
public void perform() {
if (state == STANDING) {
System.out.println("Perform the standing ability");
} else if (state == WALKING) {
System.out.println("Perform the walking ability");
} else if (state == RUNNING) {
System.out.println("Perform the running ability");
} else if (state == JUMPING) {
System.out.println("Perform the jumping ability");
}
}
public void talking() {
if (state == STANDING) {
System.out.println("Talking...");
} else if (state == WALKING) {
System.out.println("Alert: fail to talking");
} else if (state == RUNNING) {
System.out.println("Alert: fail to talking");
} else if (state == JUMPING) {
System.out.println("Alert: fail to talking");
}
}
public void setState(int state) {
this.state = state;
}
}
看到上面这个类,你可能很快就会发现问题。因为每个状态的表现形式都不一样,这个类里面的每个方法都需要对当前的状态进行判断。假设我们现在需要添加一个新的状态,那么势必我们需要对几乎所有的方法都进行修改,这显然 违背了我们之前强调的封闭修改,开放拓展原则(Close for modification,Open for extension)。
另外,在方法中使用大量的 if-else 条件判断,将所有的逻辑放在一起也是不可取的。时间长了,方法会变得非常的臃肿,上面的例子比较简单,看上去好像没啥问题,可你想想,如果每个状态下我们需要做很多事情,使用很多的临时变量,进行很多的计算和业务逻辑,那么代码还会这么简单吗?即使你在此基础之上做封装,重构,但是这一个方法依然做了很多的事情,没法做到单一职责。
还有一点就是,上面的例子并没有很复杂的状态切换,如果把状态切换的逻辑再加到每个方法中,状态之间彼此依赖,代码的复杂度还会上升。想要彻底解决这个问题,我们必须从代码的架构入手。
状态模式
上面代码问题的根源在于状态的可变性,一是状态定义的可变,比如现在只有四个状态,我们后面可能会增加或对现有的状态进行更改,二是状态之间可以相互转化,相互切换。上面的代码直接操作变化的东西,导致程序过于耦合,因此,一个思路就是,将变化的部分封装起来,这也是重构代码的一个常用手段。
因为要把状态封装起来,我们需要定义一个统一的接口:
1
2
3
4
5
interface State {
void display();
void perform();
void talking();
}
然后再将每个状态实现:
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
class StandingState implements State {
public void display() {
System.out.println("Standing...");
}
public void perform() {
System.out.println("Perform the standing ability");
}
public void talking() {
System.out.println("Talking...");
}
}
class WalkingState implements State {
public void display() {
System.out.println("Walking...");
}
public void perform() {
System.out.println("Perform the walking ability");
}
public void talking() {
System.out.println("Alert: fail to talking");
}
}
class RunningState implements State {
public void display() {
System.out.println("Running...");
}
public void perform() {
System.out.println("Perform the running ability");
}
public void talking() {
System.out.println("Alert: fail to talking");
}
}
class JumpingState implements State {
public void display() {
System.out.println("Jumping...");
}
public void perform() {
System.out.println("Perform the jumping ability");
}
public void talking() {
System.out.println("Alert: fail to talking");
}
}
状态是拆分出来了,但是更重要的是我们需要把这些状态用在游戏中,这样才能和前面的代码功能保持一致:
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
class Game {
private State standingState;
private State walkingState;
private State runningState;
private State JumpingState;
private State state;
public Game() {
standingState = new StandingState();
walkingState = new WalkingState();
runningState = new RunningState();
jumpingState = new JumpingState();
state = standingState;
}
public void display() {
state.display();
}
public void perform() {
state.perform();
}
public void talking() {
state.talking();
}
public void setState(State state) {
this.state = state;
}
}
可以看到,当我们对状态进行封装后,Game
类中的函数逻辑变得非常简单,这些方法直接调用当前状态中的方法,没有任何的条件判断。
另外,当我们需要引入新的状态时,我们也只需要新创建一个 State
接口的实现类,然后添加进 Game
即可。重点是我们不需要更改其他的状态,也不需要更改 Game
类中的方法。
下图可以很好地概括状态模式:
这里面的 Context
指的就是上面例子中的 Game
,可以看到的是状态模式主要依赖于 组合 和 委托。组合是指状态类 State
为 Context
类的成员,这样两个类不至于耦合过深。委托指的是,Context
类中的成员方法将请求委托于状态成员来执行,从而降低整体的复杂度。
状态模式 VS 策略模式
之前我们讲过 策略模式,如果你回过头去看,会发现这两个模式的结构怎么一摸一样?这不就是一个模式吗?
它们的结构一样,这没错,但是它们依然是不同的模式。它们的区别主要体现在使用上,以及所要达到的目的。
两个模式的名字其实就是很好的说明。首先,什么是策略?策略可以理解为解决方案,一般来说,对一个问题我们可以有多个解决方案,但是往往我们会从多个解决方案中选出一种来使用,这个选择是外部的,也就是说策略模式内部并不会更改外界传入的策略,它会按照外界传入的策略来执行。再来看看什么是状态,如果把状态也看作是解决方案,这显然不恰当,并且状态肯定不是单一的,如果只有单一一个状态那其实等同于没有状态,而且更重要的是状态之间可以来回切换,比如在某个行为下,我们从状态 A 变成了状态 B,这个改变也是发生在状态模式的内部。状态的更改、维护都应该发生在内部,不然在使用上会很乱。
知道了两个模式的定义,我们再来看看它们的各自的目的。策略模式主要是为了去重,如果每个策略都需要重新创建一套独立的模块,那这样就会存在重复的代码,后期维护会难上加难。策略模式提供了一套灵活的配置 Context
类的机制,并且允许运行时更改策略。
而状态模式更多的是偏重解耦,如果不加以封装,状态和状态之间的相互关联会让程序逻辑变得十分复杂,并且 Context
类中的方法也需要反复地去检查当前的逻辑。与前面策略模式配置 Context
不同,状态模式的目的并不在配置,而是允许 Context
可以根据当前状态来改变行为,这更多的发生在内部。
另外,上面的例子仅仅处于展示的目的,我们并没有引入状态的变化,而在实际中这并不常见。状态的切换可以放在 Context
类中,当然也可以放在状态的实现类中,这个需要根据实际情况来权衡。一般来说,如果状态的数目很多,变化的概率很大,最好还是放在 Context
类中,这样有一个地方统一管理,并且解除了状态相互之间的耦合,程序会比较好维护。