Scott's world.

设计模式-状态模式

Word count: 3.1kReading time: 12 min
2019/08/23 Share

设计模式-状态模式

模式动机

  • 在很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的(stateful)对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。
  • 在UML中可以使用状态图来描述对象状态的变化

模式定义

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

模式结构

状态模式包含如下角色:

  • Context: 环境类
  • State: 抽象状态类
  • ConcreteState: 具体状态类

代码实现

我这里主要运用的是鸿洋_博主使用的例子

假如我们现在有一个自动售货机的程序需要我们来设计

我们可以来分析一下这个状态图

  • 包含4个状态
  • 包含3个暴露在外的方法(即投币,退币,转动手柄)
  • 需要处理每个状态下,用户都可以触发这三个动作

接下来我们就开始书写代码,首先我们要写一个机器

VendingMachine.class

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package designDemo.statePattern;

/**
* TODO
*
* @author tudou
* @version 1.0
* @date 2019/8/23 1:41
**/
public class VendingMachine {
/**
* 已经投币
*/
private final static int HAS_BI = 0;

/**
* 没有投币
*/
private final static int NO_BI = 1;

/**
* 售出商品
*/
private final static int SOLD = 2;

/**
* 商品售空
*/
private final static int SOLD_OUT = 3;

private int currentStatus = NO_BI;

/**
* 商品数量
*/
private int count = 0;

/**
* 初始状态即未投币
*/
public VendingMachine(int count) {
this.count = count;
if (count > 0) {
currentStatus = NO_BI;
}
}

/**
* 投入硬币
*/
public void insertMoney() {
switch (currentStatus) {
case NO_BI:
currentStatus = HAS_BI;
System.out.println("投入硬币成功");
break;
case HAS_BI:
System.out.println("请勿重新投入硬币");
break;
case SOLD:
System.out.println("请稍等...");
break;
case SOLD_OUT:
System.out.println("商品售空,请勿投币");
break;
default:
System.out.println("机器出现未知故障");
}
}

/**
* 退币
*/
public void backMoney() {
switch (currentStatus) {
case NO_BI:
System.out.println("未投入硬币");
break;
case HAS_BI:
currentStatus = NO_BI;
System.out.println("退币成功");
break;
case SOLD:
System.out.println("商品已经出售,不能退币");
break;
case SOLD_OUT:
System.out.println("未投入硬币");
break;
default:
System.out.println("机器出现未知故障");

}
}

/**
* 转动手柄,购买商品
*/
public void turnCrank() {
switch (currentStatus) {
case NO_BI:
System.out.println("请先投入硬币");
break;
case HAS_BI:
System.out.println("正在出商品....");
currentStatus = SOLD;
distribute();
break;
case SOLD:
System.out.println("连续转动也没用...");
break;
case SOLD_OUT:
System.out.println("商品已经售罄");
break;
default:
System.out.println("机器出现未知故障");


}
}

/**
* 发放商品
*/
private void distribute() {

switch (currentStatus) {
case NO_BI:
case HAS_BI:
case SOLD_OUT:
throw new IllegalStateException("非法的状态");
case SOLD:
count--;
System.out.println("发出商品");
if (count == 0) {
System.out.println("商品售罄");
currentStatus = SOLD_OUT;
} else {
currentStatus = NO_BI;
}
break;
default:
System.out.println("机器出现未知故障");

}
}


}

针对其每一个动作,我们都需要考虑其有可能任何情况都会发生,下面进行一些测试

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
package designDemo.statePattern;

/**
* @author tudou
* @version 1.0
* @date 2019/8/23 1:52
**/
public class TestTra {
public static void main(String[] args)
{
VendingMachine machine = new VendingMachine(10);
machine.insertMoney();
machine.backMoney();
System.out.println("-----------正常购买流程");
machine.insertMoney();
machine.turnCrank();
System.out.println("----------压力测试-----");
machine.insertMoney();
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.backMoney();
machine.turnCrank();

}

}

其输出结果为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
投入硬币成功
退币成功
-----------正常购买流程
投入硬币成功
正在出商品....
发出商品
----------压力测试-----
投入硬币成功
请勿重新投入硬币
正在出商品....
发出商品
投入硬币成功
退币成功
请先投入硬币

以上我们就成功设计了一个在一个自动售货机上面购买一个商品的流程,这一流程的所有行为都是具有状态性的,每一个动作下都有不同的状态

但是现在我们老板需要增加一个新的需求,也就是增加一个中奖的需求,来提升销量,即当用户每次转动手柄购买商品时有10%的几率会赠送额外的一瓶

现在的状态图发生了变化,当用户转动手柄时,可能会达到一个中奖的状态:图如下:

如果这在我们上面的代码里直接添加,则需要再每个动作的switch中添加判断条件,虽然可以实现,但是如何以后有更加复杂的状态而影响每一个动作的话,我们的出错率就会增加

所以现在我们可以重新考虑我们的代码,进行重构,我们考虑将每个状态写成状态类,负责实现现在对应动作下的行为,然后在自动售货机在状态间相互切换

下面开始重构,我们现在有5种状态,对应4个动作(投币、退币、转动手柄、发出商品),下面首先定义一个状态的超类型:

State.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 状态的接口
*/
public interface State
{
/**
* 放钱
*/
public void insertMoney();
/**
* 退钱
*/
public void backMoney();
/**
* 转动曲柄
*/
public void turnCrank();
/**
* 出商品
*/
public void dispense();
}

然后是每个状态的实现:

NoMoneyState.class

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
package com.zhy.pattern.status.b;

/**
* 没钱的状态
*/
public class NoMoneyState implements State
{

private VendingMachine machine;

public NoMoneyState(VendingMachine machine)
{
this.machine = machine;

}

@Override
public void insertMoney()
{
System.out.println("投币成功");
machine.setState(machine.getHasMoneyState());
}

@Override
public void backMoney()
{
System.out.println("您未投币,想退钱?...");
}

@Override
public void turnCrank()
{
System.out.println("您未投币,想拿东西么?...");
}

@Override
public void dispense()
{
throw new IllegalStateException("非法状态!");
}

}

HasMoneyState.class

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
/**
* 已投入钱的状态
*/
public class HasMoneyState implements State
{

private VendingMachine machine;
private Random random = new Random();

public HasMoneyState(VendingMachine machine)
{
this.machine = machine;
}

@Override
public void insertMoney()
{
System.out.println("您已经投过币了,无需再投....");
}

@Override
public void backMoney()
{
System.out.println("退币成功");

machine.setState(machine.getNoMoneyState());
}

@Override
public void turnCrank()
{
System.out.println("你转动了手柄");
int winner = random.nextInt(10);
if (winner == 0 && machine.getCount() > 1)
{
machine.setState(machine.getWinnerState());
} else
{
machine.setState(machine.getSoldState());
}
}

@Override
public void dispense()
{
throw new IllegalStateException("非法状态!");
}

}

SoldOutState.class

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
/**
* 售空的状态
*/
public class SoldOutState implements State
{

private VendingMachine machine;

public SoldOutState(VendingMachine machine)
{
this.machine = machine;
}

@Override
public void insertMoney()
{
System.out.println("投币失败,商品已售空");
}

@Override
public void backMoney()
{
System.out.println("您未投币,想退钱么?...");
}

@Override
public void turnCrank()
{
System.out.println("商品已售空,转动手柄也木有用");
}

@Override
public void dispense()
{
throw new IllegalStateException("非法状态!");
}

}

SoldState.class

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
 /**
* 准备出商品的状态,该状态下,不会有任何用户的操作
*/
public class SoldState implements State
{

private VendingMachine machine;

public SoldState(VendingMachine machine)
{
this.machine = machine;
}

@Override
public void insertMoney()
{
System.out.println("正在出货,请勿投币");
}

@Override
public void backMoney()
{
System.out.println("正在出货,没有可退的钱");
}

@Override
public void turnCrank()
{
System.out.println("正在出货,请勿重复转动手柄");
}

@Override
public void dispense()
{
machine.dispense();
if (machine.getCount() > 0)
{
machine.setState(machine.getNoMoneyState());
} else
{
System.out.println("商品已售空");
machine.setState(machine.getSoldOutState());
}
}
}

WinnerState.class

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
/**
* 中奖的状态,该状态下不会有任何用户的操作
*/
public class WinnerState implements State
{

private VendingMachine machine;

public WinnerState(VendingMachine machine)
{
this.machine = machine;
}

@Override
public void insertMoney()
{
throw new IllegalStateException("非法状态");
}

@Override
public void backMoney()
{
throw new IllegalStateException("非法状态");
}

@Override
public void turnCrank()
{
throw new IllegalStateException("非法状态");
}

@Override
public void dispense()
{
System.out.println("你中奖了,你将获得两件商品");
machine.dispense();

if (machine.getCount() == 0)
{
System.out.println("商品已经售空");
machine.setState(machine.getSoldOutState());
} else
{
machine.dispense();
if (machine.getCount() > 0)
{
machine.setState(machine.getNoMoneyState());
} else
{
System.out.println("商品已经售空");
machine.setState(machine.getSoldOutState());
}

}

}

}

最后是自动售货机的代码:

VendingMachine.class

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
/**
* 自动售货机
*/
public class VendingMachine
{
private State noMoneyState;
private State hasMoneyState;
private State soldState;
private State soldOutState;
private State winnerState ;

private int count = 0;
private State currentState = noMoneyState;

public VendingMachine(int count)
{
noMoneyState = new NoMoneyState(this);
hasMoneyState = new HasMoneyState(this);
soldState = new SoldState(this);
soldOutState = new SoldOutState(this);
winnerState = new WinnerState(this);

if (count > 0)
{
this.count = count;
currentState = noMoneyState;
}
}

public void insertMoney()
{
currentState.insertMoney();
}

public void backMoney()
{
currentState.backMoney();
}

public void turnCrank()
{
currentState.turnCrank();
if (currentState == soldState || currentState == winnerState)
currentState.dispense();
}

public void dispense()
{
System.out.println("发出一件商品...");
if (count != 0)
{
count -= 1;
}
}

public void setState(State state)
{
this.currentState = state;
}

//getter setter omitted ...

}

下面进行测试:

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 Test
{
public static void main(String[] args)
{
VendingMachine machine = new VendingMachine(10);
machine.insertMoney();
machine.backMoney();
System.out.println("----我要中奖");
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
System.out.println("-------压力测试");
machine.insertMoney();
machine.backMoney();
machine.turnCrank();
machine.backMoney();

}
}

至于测试结果我就不放上来了

现在可以看到,我们现在把每个状态对应于动作的行为局部化到了状态自己的类中实现,不仅增加了扩展性而且使代码的阅读性大幅度的提高。以后如果增加了新的状态,只需要在接口处添加状态然后实现状态,并在自动售货机中添加这个状态就行了

模式分析

  • 状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为。
  • 状态模式的关键是引入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换。

模式优缺点

优点

  • 封装了转换规则。
  • 枚举可能的状态,在枚举状态之前需要确定状态种类。
  • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
  • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

缺点

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

适用环境

在下面的两种情况下均可使用State模式:

  • 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
  • 代码中包含大量与对象状态有关的条件语句:一个操作中含有庞大的多分支的条件(if else(或switch case)语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。 State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化

状态模式在工作流或游戏等类型的软件中得以广泛使用,甚至可以用于这些系统的核心功能设计,如在政府OA办公系统中,一个批文的状态有多种:尚未办理;正在办理;正在批示;正在审核;已经完成等各种状态,而且批文状态不同时对批文的操作也有所差异。使用状态模式可以描述工作流对象(如批文)的状态转换以及不同状态下它所具有的行为。

参考

https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/state.html

https://blog.csdn.net/lmj623565791/article/details/26350617

CATALOG
  1. 1. 设计模式-状态模式
    1. 1.1. 模式动机
    2. 1.2. 模式定义
    3. 1.3. 模式结构
    4. 1.4. 代码实现
    5. 1.5. 模式分析
    6. 1.6. 模式优缺点
      1. 1.6.1. 优点
      2. 1.6.2. 缺点
    7. 1.7. 适用环境
    8. 1.8. 参考