Hexo

点滴积累 豁达处之

0%

18 状态模式

​ 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

状态模式

1 概述

​ 状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Object for States),状态模式是一种对象行为型模式

2 引入

​ 我们每天都在乘电梯,那我们来看看电梯有哪些动作(映射到Java 中就是有多少方法):开门、关门、运行、停止,就这四个动作,好,我们就用程序来实现一下电梯的动作,先看类图设计:

状态模式1

​ 非常简单的类图,定义一个接口,然后是一个实现类,然后业务类Client 就可以调用,并运行起来,简单也来看看我们的程序,先看接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 定义一个电梯的接口
*/
public interface ILift {
//首先电梯门开启动作
public void open();
//电梯门有开启,那当然也就有关闭了
public void close();
//电梯要能上能下,跑起来
public void run();
//电梯还要能停下来,停不下来那就扯淡了
public void stop();
}

实现类:

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

/**
* 电梯的实现类
*/
public class Lift implements ILift {
//电梯门关闭
public void close() {
System.out.println("电梯门关闭...");
}
//电梯门开启
public void open() {
System.out.println("电梯门开启...");
}
//电梯开始跑起来
public void run() {
System.out.println("电梯上下跑起来...");
}
//电梯停止
public void stop() {
System.out.println("电梯停止了...");
}
}

电梯的开、关、跑、停都实现了,开看业务是怎么调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 模拟电梯的动作
*/
public class Client {
public static void main(String[] args) {
ILift lift = new Lift();
//首先是电梯门开启,人进去
lift.open();
//然后电梯门关闭
lift.close();
//再然后,电梯跑起来,向上或者向下
lift.run();
//最后到达目的地,电梯挺下来
lift.stop();
}
}

运行的结果如下:

1
2
3
4
电梯门开启...
电梯门关闭...
电梯上下跑起来...
电梯停止了...

​ 仔细想想,电梯的这四个动作的执行都是有前置条件,具体点说说在特定状态下才能做特定事,那我们来分析一下电梯有什么那些特定状态:
门敞状态—

​ 按了电梯上下按钮,电梯门开,这中间有5 秒的时间(当然你也可以用身体挡住电梯门,那就不是5 秒了),那就是门敞状态;在这个状态下电梯只能做的动作是关门动作,做别的动作?那就危险喽

 门闭状态—

电梯门关闭了,在这个状态下,可以进行的动作是:开门(我不想坐电梯了)、停止(忘记按路层号了)、运行
运行状态—

​ 电梯正在跑,上下窜,在这个状态下,电梯只能做的是停止;
停止状态—

​ 电梯停止不动,在这个状态下,电梯有两个可选动作:继续运行和开门动作;

我们用一张表来表示电梯状态和动作之间的关系:

状态模式2

​ 电梯状态和动作对应表(○表示不允许,☆表示允许动作)

我们来修改一下,先看类图:

状态模式3

  增加了状态的类图

​ 在接口中定义了四个常量,分别表示电梯的四个状态:门敞状态、关闭状态、运行状态、停止状态,然后在实现类中电梯的每一次动作发生都要对状态进行判断,判断是否运行执行,也就是动作的执行是否符合业务逻辑,实现类中的四个私有方法是仅仅实现电梯的动作,没有任何的前置条件,因此这四个方法是不能为外部类调用的,设置为私有方法。我们先看接口的改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 定义一个电梯的接口
*/
public interface ILift {
//电梯的四个状态
public final static int OPENING_STATE = 1; //门敞状态
public final static int CLOSING_STATE = 2; //门闭状态
public final static int RUNNING_STATE = 3; //运行状态
public final static int STOPPING_STATE = 4; //停止状态;
//设置电梯的状态
public void setState(int state);
//首先电梯门开启动作
public void open();
//电梯门有开启,那当然也就有关闭了
public void close();
//电梯要能上能下,跑起来
public void run();
//电梯还要能停下来,停不下来那就扯淡了
public void stop();
}

增加了四个静态常量,增加了一个方法setState,设置电梯的状态。我们再来看实现类是如何实现的:

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
/**
* 电梯的实现类
*/
public class Lift implements ILift {
private int state;
public void setState(int state) {
this.state = state;
}
//电梯门关闭
public void close() {
//电梯在什么状态下才能关闭
switch(this.state){
case OPENING_STATE: //如果是则可以关门,同时修改电梯状态
this.closeWithoutLogic();
this.setState(CLOSING_STATE);
break;
case CLOSING_STATE: //如果电梯就是关门状态,则什么都不做
//do nothing;
break;
case RUNNING_STATE: //如果是正在运行,门本来就是关闭的,也说明都不做
//do nothing;
break;
case STOPPING_STATE: //如果是停止状态,本也是关闭的,什么也不做
//do nothing;
break;
}
}
//电梯门开启
public void open() {
//电梯在什么状态才能开启
switch(this.state){
case OPENING_STATE: //如果已经在门敞状态,则什么都不做
//do nothing;
break;
case CLOSING_STATE: //如是电梯时关闭状态,则可以开启
this.openWithoutLogic();
this.setState(OPENING_STATE);
break;
case RUNNING_STATE: //正在运行状态,则不能开门,什么都不做
//do nothing;
break;
case STOPPING_STATE: //停止状态,淡然要开门了
this.openWithoutLogic();
this.setState(OPENING_STATE);
break;
}
}
//电梯开始跑起来
public void run() {
switch(this.state){
case OPENING_STATE: //如果已经在门敞状态,则不你能运行,什么都不做
//do nothing;
break;
case CLOSING_STATE: //如是电梯时关闭状态,则可以运行
this.runWithoutLogic();
this.setState(RUNNING_STATE);
break;
case RUNNING_STATE: //正在运行状态,则什么都不做
//do nothing;
break;
case STOPPING_STATE: //停止状态,可以运行
this.runWithoutLogic();
this.setState(RUNNING_STATE);
}
}
//电梯停止
public void stop() {
switch(this.state){
case OPENING_STATE: //如果已经在门敞状态,那肯定要先停下来的,什么都不做
//do nothing;
break;
case CLOSING_STATE: //如是电梯时关闭状态,则当然可以停止了
this.stopWithoutLogic();
this.setState(CLOSING_STATE);
break;
case RUNNING_STATE: //正在运行状态,有运行当然那也就有停止了
this.stopWithoutLogic();
this.setState(CLOSING_STATE);
break;
case STOPPING_STATE: //停止状态,什么都不做
//do nothing;
break;
}
}
//纯粹的电梯关门,不考虑实际的逻辑
private void closeWithoutLogic(){
System.out.println("电梯门关闭...");
}
//纯粹的店门开,不考虑任何条件
private void openWithoutLogic(){
System.out.println("电梯门开启...");
}
//纯粹的运行,不考虑其他条件
private void runWithoutLogic(){
System.out.println("电梯上下跑起来...");
}
//单纯的停止,不考虑其他条件
private void stopWithoutLogic(){
System.out.println("电梯停止了...");
}
}

程序有点长,但是还是很简单的,就是在每一个接口定义的方法中使用witch…case 来进行判断,是否运行运行指定的动作。我们来Client 程序的变更:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 模拟电梯的动作
*/
public class Client {
public static void main(String[] args) {
ILift lift = new Lift();
//电梯的初始条件应该是停止状态
lift.setState(ILift.STOPPING_STATE);
//首先是电梯门开启,人进去
lift.open();
//然后电梯门关闭
lift.close();
//再然后,电梯跑起来,向上或者向下
lift.run();
//最后到达目的地,电梯停下来
lift.stop();
}
}

业务调用的方法中增加了电梯状态判断,电梯要开门不是随时都可以开的,必须满足了一定条件你才能开门,人才能走进去,我们设置电梯的起始是停止状态,看运行结果:

1
2
3
4
电梯门开启...
电梯门关闭...
电梯上下跑起来...
电梯停止了...

​ 我们来想一下,这段程序有什么问题,首先Lift.java 这个文件有点长,长的原因是我们在程序中使用了大量的switch…case 这样的判断(if…else 也是一样),程序中只要你有这样的判断就避免不了加长程序,同步的在业务比较复杂的情况下,程序体会更长,这个就不是一个很好的习惯了,较长的方法或者类的维护性比较差,毕竟程序是给人来阅读的;其次,扩展性非常的不好,大家来想想,电梯还有两个状态没有加,是什么?通电状态和断电状态,你要是在程序再增加这两个方法,你看看Open()、Close()、Run()、Stop()这四个方法都要增加判断条件,也就是说switch 判断体中还要增加case 项,也就说与开闭原则相违背了;再其次,我们来思考我们的业务,电梯在门敞开状态下就不能上下跑了吗?电梯有没有发生过只有运行没有停止状态呢(从40 层直接坠到1 层嘛)?电梯故障嘛,还有电梯在检修的时候,可以在stop状态下不开门,这也是正常的业务需求呀,你想想看,如果加上这些判断条件,上面的程序有多少需要修改?虽然这些都是电梯的业务逻辑,但是一个类有且仅有一个原因引起类的变化,单一职责原则,看看我们的类,业务上的任务一个小小增加或改动都对我们的这个电梯类产生了修改,这是在项目开发上是有很大风险的。既然我们已经发现程序上有以上问题,我们怎么来修改呢?

​ 刚刚我们是从电梯的有哪些方法以及这些方法执行的条件去分析,现在我们换个角度来看问题,我们来想电梯在具有这些状态的时候,能够做什么事情,也就是说在电梯处于一个具体状态时,我们来思考这个状态是由什么动作触发而产生以及在这个状态下电梯还能做什么事情,举个例子来说,电梯在停止状态时,我们来思考两个问题:

​ 1.这个停止状态时怎么来的,那当然是由于电梯执行了stop 方法而来的;       

​ 2.在停止状态下,电梯还能做什么动作?继续运行?开门?那当然都可以了。

​ 我们再来分析其他三个状态,也都是一样的结果,我们只要实现电梯在一个状态下的两个任务模型就可以了:这个状态是如何产生的以及在这个状态下还能做什么其他动作(也就是这个状态怎么过渡到其他状态),

3 示例

我们以状态为参考模型,那我们就先定义电梯的状态接口,思考过后我们来看类图:

状态模式4

​ 在类图中,定义了一个LiftState 抽象类,声明了一个受保护的类型Context 变量,这个是串联我们各个状态的封装类,封装的目的很明显,就是电梯对象内部状态的变化不被调用类知晓,也就是迪米特法则了,我的类内部情节你知道越少越好,并且还定义了四个具体的实现类,承担的是状态的产生以及状态间的转换过渡,我们先来看LiftState 程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 定义一个电梯的接口
*/
public abstract class LiftState{
//定义一个环境角色,也就是封装状态的变换引起的功能变化
protected Context context;
public void setContext(Context _context){
this.context = _context;
}
//首先电梯门开启动作
public abstract void open();
//电梯门有开启,那当然也就有关闭了
public abstract void close();
//电梯要能上能下,跑起来
public abstract void run();
//电梯还要能停下来,停不下来那就扯淡了
public abstract void stop();
}

抽象类比较简单,我们来先看一个具体的实现,门敞状态的实现类:

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
/**
* 在电梯门开启的状态下能做什么事情
*/
public class OpenningState extends LiftState {
//开启当然可以关闭了,我就想测试一下电梯门开关功能
@Override
public void close() {
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行
super.context.getLiftState().close();
}

//打开电梯门
@Override
public void open() {
System.out.println("电梯门开启...");
}

//门开着电梯就想跑,这电梯,吓死你!
@Override
public void run() {
//do nothing;
}

//开门还不停止?
public void stop() {
//do nothing;
}
}

​ 我来解释一下这个类的几个方法,Openning 状态是由open()方法产生的,因此这个方法中有一个具体的业务逻辑,我们是用print 来代替了;在Openning 状态下,电梯能过渡到其他什么状态呢?按照现在的定义的是只能过渡到Closing 状态,因此我们在Close()中定义了状态变更,同时把Close 这个动作也委托了给CloseState 类下的Close 方法执行,这个可能不好理解,我们再看看Context 类就可能好理解一点:

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

public class Context {
//定义出所有的电梯状态
public final static OpenningState openningState = new OpenningState();
public final static ClosingState closeingState = new ClosingState();
public final static RunningState runningState = new RunningState();
public final static StoppingState stoppingState = new StoppingState();
//定一个当前电梯状态
private LiftState liftState;
public LiftState getLiftState() {
return liftState;
}
public void setLiftState(LiftState liftState) {
this.liftState = liftState;
//把当前的环境通知到各个实现类中
this.liftState.setContext(this);
}
public void open(){
this.liftState.open();
}
public void close(){
this.liftState.close();
}
public void run(){
this.liftState.run();
}
public void stop(){
this.liftState.stop();
}
}

​ 结合以上三个类,我们可以这样理解,Context 是一个环境角色,它的作用是串联各个状态的过渡,在
LiftSate 抽象类中我们定义了并把这个环境角色聚合进来,并传递到了子类,也就是四个具体的实现类中

​ 自己根据环境来决定如何进行状态的过渡。我们把其他的三个具体实现类阅读完毕,下面是关闭状态:

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
/**
* 电梯门关闭以后,电梯可以做哪些事情
*/
public class ClosingState extends LiftState {
//电梯门关闭,这是关闭状态要实现的动作
@Override
public void close() {
System.out.println("电梯门关闭...");
}
//电梯门关了再打开,逗你玩呢,那这个允许呀
@Override
public void open() {
super.context.setLiftState(Context.openningState); //置为门敞状态
super.context.getLiftState().open();
}
//电梯门关了就跑,这是再正常不过了
@Override
public void run() {
super.context.setLiftState(Context.runningState); //设置为运行状态;
super.context.getLiftState().run();
}
//电梯门关着,我就不按楼层
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState); //设置为停止状态;
super.context.getLiftState().stop();
}
}

下面是电梯的运行状态:

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
/**
* 电梯在运行状态下能做哪些动作
*/
public class RunningState extends LiftState {
//电梯门关闭?这是肯定了
@Override
public void close() {
//do nothing
}
//运行的时候开电梯门?你疯了!电梯不会给你开的
@Override
public void open() {
//do nothing
}
//这是在运行状态下要实现的方法
@Override
public void run() {
System.out.println("电梯上下跑...");
}
//这个事绝对是合理的,光运行不停止还有谁敢做这个电梯?!估计只有上帝了
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState); //环境设置为停止状态;
super.context.getLiftState().stop();
}
}

下面是停止状态:

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
/**
* 在停止状态下能做什么事情
*/
public class StoppingState extends LiftState {
//停止状态关门?电梯门本来就是关着的!
@Override
public void close() {
//do nothing;
}
//停止状态,开门,那是要的!
@Override
public void open() {
super.context.setLiftState(Context.openningState);
super.context.getLiftState().open();
}
//停止状态再跑起来,正常的很
@Override
public void run() {
super.context.setLiftState(Context.runningState);
super.context.getLiftState().run();
}
//停止状态是怎么发生的呢?当然是停止方法执行了
@Override
public void stop() {
System.out.println("电梯停止了...");
}
}

业务逻辑都已经实现了,我们来看看Client 怎么实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 模拟电梯的动作
*/
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.setLiftState(new ClosingState());
context.open();
context.close();
context.run();
context.stop();
}
}

Client 调用类太简单了,只要定义个电梯的初始状态,然后调用相关的方法,就完成了,完全不用考 虑状态的变更,看运行结果:

1
2
3
4
电梯门开启...
电梯门关闭...
电梯上下跑起来...
电梯停止了...

状态模式封装的非常好,状态的变更引起了行为的变更,从外部看起来就好像这个对象对应的类发生了改变一样

4 状态模式优缺点

优点:

1)面向接口式编程,将实现细节巧妙封装在各个不同的状态类中,状态转换交给状态类自己去实现,外部无需关心;
2)将由大量业务、大量逻辑判断的代码去除,状态类内部通过状态的转换实现相关逻辑,代码可读性更好;

缺点:

1)增加新的状态时会增加状态类,而且在增加新的状态类之后,环境类需要做相应的修改,不太符合开闭原则;