设计模式之状态模式

设计模式之状态模式

糖果机的需求

我们公司接到糖果公司的需求,要设计一个糖果机的程序,糖果机有四个状态,未投币,已投币,售出,售罄这四个状态,当客户投币后,就开始转动曲柄,然后发放糖果。当糖果售罄时,就不在允许投入币了。所以希望我们设计能够尽量有弹性而且好维护的程序,在将来有可能增加更多的行为。

糖果机初版代码实现

现在我们来实现糖果机。我们知道要利用实例变量持有当前的状态,然后需要处理所有可能发生的动作、行为和状态的转换。我们需要实现的工作包括:投币、退币、转动曲柄和发放糖果;也需要检查糖果是否售罄。

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
/**
* 糖果机
* @author Mr.zxb
* @date 2020-09-06 21:18:14
*/
public class GumballMachine {
// 售罄
public static final int SOLD_OUT = 0;
// 没有投币
public static final int NO_QUARTER = 1;
// 已投币
public static final int HAS_QUARTER = 2;
// 售出
public static final int SOLD = 3;
// 当前状态
private int state = SOLD_OUT;
// 糖果数量
private int count = 0;

public GumballMachine(int count) {
this.count = count;
if (count > 0) {
state = NO_QUARTER;
}
}

/**
* 投币方法
*/
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("You can't insert another quarter.");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("You inserted a quarter.");
} else if (state == SOLD_OUT) {
System.out.println("You cant't insert a quarter, the machine is sold out.");
} else if (state == SOLD) {
System.out.println("Please wait, we're already giving you a gumball.");
}
}

/**
* 退币方法
*/
public void ejectQuarter() {
if (state == HAS_QUARTER) {
System.out.println("Quarter returned.");
state = NO_QUARTER;
} else if (state == NO_QUARTER) {
System.out.println("You haven't inserted a quarter.");
} else if (state == SOLD) {
System.out.println("Sorry, you already turned the crank.");
} else if (state == SOLD_OUT) {
System.out.println("You can't eject, you haven't inserted a quarter yet.");
}
}

/**
* 转动曲柄方法
*/
public void turnCrank() {
if (state == SOLD) {
System.out.println("Turning twice doesn't get you another gumball.");
} else if (state == NO_QUARTER) {
System.out.println("You turned but there's no quarter.");
} else if (state == SOLD_OUT) {
System.out.println("You turned, but there are no gumballs.");
} else if (state == HAS_QUARTER) {
System.out.println("You turned...");
state = SOLD;
dispense();
}
}

/**
* 发放糖果方法
*/
public void dispense() {
if (state == SOLD) {
System.out.println("A gumball comes rolling out the solt.");
count--;
if (count == 0) {
System.out.println("Oops, out of gumballs.");
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
} else if (state == NO_QUARTER) {
System.out.println("You need to pay first.");
} else if (state == SOLD_OUT) {
System.out.println("No gumball dispensed.");
} else if (state == HAS_QUARTER) {
System.out.println("No gumball dispensed.");
}
}

@Override
public String toString() {
return "GumballMachine{" +
"state=" + state +
", count=" + count +
'}';
}
}

测试糖果机

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 GumballMachineTestDrive {
public static void main(String[] args) {
// 创建装有5个糖果的糖果机
GumballMachine gumballMachine = new GumballMachine(5);

// 打印机器状态
System.out.println(gumballMachine);

// 投币
gumballMachine.insertQuarter();
// 转动曲柄
gumballMachine.turnCrank();

// 再次打印机器状态
System.out.println(gumballMachine);

// 投币
gumballMachine.insertQuarter();
// 退币
gumballMachine.ejectQuarter();
// 转动曲柄
gumballMachine.turnCrank();

// 再次打印机器状态
System.out.println(gumballMachine);
}
}

新的需求

糖果公司在程序运行一段时间后,想要新增一个功能,有一个幸运玩家,就是售出糖果的时候,有10%的几率会掉出2课糖果,而不是一颗。

面对原有的程序,我们就必须要新增一个状态,称为“赢家”。还不太麻烦,不过在每个方法的条件判断里处理“赢家”状态;这可有的忙了。随着新的行为出现,这个程序越来越难以适用。

新的设计

我们的计划是:不要维护我们现有的代码,我们重写它以便于将状态对象封装在各自的类中,然后在动作发生时委托给当前状态。

  1. 首先,我们定义一个State接口。在这个接口内,糖果机的每个动作都有一个对应的方法。
  2. 然后为机器中的每个状态实现状态类。这些类将负责对应的状态下进行机器的行为
  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
/**
* 状态接口
* @author Mr.zxb
* @date 2020-09-06 22:00:14
*/
public interface State {
/**
* 投币
*/
void insertQuarter();

/**
* 退币方法
*/
void ejectQuarter();

/**
* 转动曲柄方法
*/
void turnCrank();

/**
* 发放糖果方法
*/
void dispense();
}
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
/**
* 未投币状态
* @author Mr.zxb
* @date 2020-09-06 22:01:26
*/
public class NoQuarterState implements State {

private final GumballMachine gumballMachine;

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

@Override
public void insertQuarter() {
System.out.println("You inserted a quarter.");
gumballMachine.setState(gumballMachine.getHasQuarterState());
}

@Override
public void ejectQuarter() {
System.out.println("You haven't inserted a quarter.");
}

@Override
public void turnCrank() {
System.out.println("You turned, but there's no quarter.");
}

@Override
public void dispense() {
System.out.println("You need to pay first.");
}
}
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
/**
* 已投币状态
* @author Mr.zxb
* @date 2020-09-06 22:09:08
*/
public class HasQuarterState implements State {

private final GumballMachine gumballMachine;

private Random random = new Random(System.currentTimeMillis());

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

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

@Override
public void ejectQuarter() {
System.out.println("Quarter returned.");
gumballMachine.setState(gumballMachine.getNoQuarterState());
}

@Override
public void turnCrank() {
System.out.println("You turned...");
int winner = random.nextInt(10);
if (winner == 0 && gumballMachine.getCount() > 1) {
gumballMachine.setState(gumballMachine.getWinnerState());
} else {
gumballMachine.setState(gumballMachine.getSoldState());
}
}

@Override
public void dispense() {
System.out.println("No gumball dispensed.");
}
}
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
/**
* 售罄状态
* @author Mr.zxb
* @date 2020-09-06 22:08:39
*/
public class SoldOutState implements State {

private final GumballMachine gumballMachine;

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

@Override
public void insertQuarter() {
System.out.println("You can't insert a quarter, the machine is sold out.");
}

@Override
public void ejectQuarter() {
System.out.println("You can't eject, you haven't inserted a quarter yet.");
}

@Override
public void turnCrank() {
System.out.println("You turned, but there are no gumballs.");
}

@Override
public void dispense() {
System.out.println("No gumball dispensed.");
}
}
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
/**
* 售出状态
* @author Mr.zxb
* @date 2020-09-06 22:09:19
*/
public class SoldState implements State {

private final GumballMachine gumballMachine;

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

@Override
public void insertQuarter() {
System.out.println("Please wait, we're already giving you a gumball.");
}

@Override
public void ejectQuarter() {
System.out.println("Sorry, you already turned the crank.");
}

@Override
public void turnCrank() {
System.out.println("Turning twice doesn't get you another gumball.");
}

@Override
public void dispense() {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
System.out.println("Oops, out of gumballs!");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
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
/**
* 幸运玩家状态
* @author Mr.zxb
* @date 2020-09-06 22:20:51
*/
public class WinnerState implements State {

private final GumballMachine gumballMachine;

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

@Override
public void insertQuarter() {
// 省略...
}

@Override
public void ejectQuarter() {
// 省略...
}

@Override
public void turnCrank() {
// 省略...
}

@Override
public void dispense() {
System.out.println("YOU'RE A WINNER! You get two gumballs for you quarter.");
gumballMachine.releaseBall();
if (gumballMachine.getCount() == 0) {
gumballMachine.setState(gumballMachine.getSoldOutState());
} else {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
System.out.println("Oops, out of gumballs!");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
}

改造糖果机

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
/**
* 糖果机
* @author Mr.zxb
* @date 2020-09-06 21:18:14
*/
public class GumballMachine {
// 售罄
private State soldOutState;
// 没有投币
private State noQuarterState;
// 已投币
private State hasQuarterState;
// 售出
private State soldState;

// 赢家
private State winnerState;

// 当前状态
private State state = soldOutState;
// 糖果数量
private int count = 0;

public GumballMachine(int count) {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
winnerState = new WinnerState(this);
this.count = count;
if (count > 0) {
state = noQuarterState;
}
}

/**
* 投币方法
*/
public void insertQuarter() {
state.insertQuarter();
}

/**
* 退币方法
*/
public void ejectQuarter() {
state.ejectQuarter();
}

/**
* 转动曲柄方法
*/
public void turnCrank() {
state.turnCrank();
state.dispense();
}

/**
* 发放糖果方法
*/
public void dispense() {
state.dispense();
}

public void releaseBall() {
System.out.println("A gumball comes rolling out the slot.");
if (count != 0) {
count--;
}
}

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

public State getState() {
return state;
}

public State getSoldOutState() {
return soldOutState;
}

public State getNoQuarterState() {
return noQuarterState;
}

public State getHasQuarterState() {
return hasQuarterState;
}

public State getSoldState() {
return soldState;
}

public int getCount() {
return count;
}

public State getWinnerState() {
return winnerState;
}
}

展示新程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GumballMachineTestDrive {
public static void main(String[] args) {
GumballMachine gumballMachine = new GumballMachine(5);
// 打印机器状态
System.out.println(gumballMachine);

gumballMachine.insertQuarter();
gumballMachine.turnCrank();
// 打印机器状态
System.out.println(gumballMachine);

gumballMachine.insertQuarter();
gumballMachine.turnCrank();
// 打印机器状态
System.out.println(gumballMachine);
}
}

总结

  • 状态模式允许一个对象基于内部状态而拥有不同的行为
  • 和程序状态机不同,状态模式用类代表状态
  • Context会将行为委托给当前状态对象
  • 通过将每个状态封装进一个类,我们把以后需要做的任何改变局部化了
  • 状态模式和策略模式有不同的类图,但是它们的意图不同
  • 策略模式通常会用行为或算法来配置Context类
  • 状态模式允许Context随着状态的改变而改变行为
  • 状态模式可以由State类或Context类控制
  • 使用状态模式通常会导致设计中的类数目大量增加
  • 状态类可以被多个Context实例共享