《Head First 设计模式》笔记9

状态模式(State)

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

栗子

现在有一台糖果机,它的状态(挺复杂的):

  • 没有 25 分钱 -> 投入 25 分钱 -> 有 25 分钱
  • 有 25 分钱 -> 转动曲柄 -> 售出糖果(数量不为0) | 糖果售罄(数量为0)
  • 有 25 分钱 -> 退钱按钮 -> 退出 25 分钱
  • 售出糖果 -> 没有 25 分钱

从上面的状态实现代码的步骤:

  1. 找出所有状态 -> 共四种:没有 25 分钱、有 25 分钱、售出糖果、糖果售罄。
  2. 创建一个持有当前状态的实例变量 state。
  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
class GumballMachine {
private final static int SOLD_OUT = 0;
private final static int NO_QUARTER = 1;
private final static int HAS_QUARTER = 2;
private final static int SOLD = 3;

private int state = SOLD_OUT; // 当前状态
private int count = 0; // 糖果数目

public GumballMachine(int count) {
this.count = count;
// 初始糖果机的糖果数
if (count > 0) {
// 大于0表示等待别人投币
state = NO_QUARTER;
}
}

// 投进25分钱
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("你已经投过25分钱了,请不要重复投币");
} else if (state == NO_QUARTER) {
System.out.println("投进25分钱");
state = HAS_QUARTER;
} else if (state == SOLD_OUT) {
System.out.println("没有糖果了,不要投币");
} else if (state == SOLD) {
System.out.println("请稍等,正在出糖果");
}
}

// 客户尝试退钱
public void ejectQuarter() {
if (state == HAS_QUARTER) {
System.out.println("已退还25分钱");
state = NO_QUARTER;
} else if (state == NO_QUARTER) {
System.out.println("你还没投币呢");
} else if (state == SOLD_OUT) {
System.out.println("没有糖果时无法投币,不要骗钱");
} else if (state == SOLD) {
System.out.println("你已经转动曲柄了,无法退钱");
}
}

// 客户转动曲柄
public void turnCrank() {
if (state == HAS_QUARTER) {
System.out.println("你转动了曲柄");
state = SOLD;
dispense();
} else if (state == NO_QUARTER) {
System.out.println("你还没投币呢");
} else if (state == SOLD_OUT) {
System.out.println("已经没有糖果了");
} else if (state == SOLD) {
System.out.println("转几次都只能拿一次");
}
}

// 糖果机发糖果
public void dispense() {
if (state == SOLD) {
System.out.println("糖果已发出");
count--;
if (count == 0) {
System.out.println("已经没有糖果喽");
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
} else if (state == NO_QUARTER) {
System.out.println("你还没投币呢");
} else if (state == SOLD_OUT) {
System.out.println("没有糖果发");
} else if (state == SOLD) {
System.out.println("没有糖果发");
}
}
}

多么缜密的判断,基本上没什么漏洞了。

新需求

现在产品经理提需求来了:当曲柄被转动时,有 10% 的几率掉出两个糖果。

看回上面缜密的代码,是不是有点无从入手?这个需求真是要了命了……如果需求再有变更,上面的代码基本上就得推倒重来了,因为代码逻辑判断太复杂。

满足需求

我们可以把这四个状态当作一个实体,比如糖果机当前处在没投币的状态,那么在该状态下,可以通过投币动作使糖果机转移到投了币状态,其他状态也是这样,基本上可以把 if else 这些判断语句分离出来。

定义状态接口:

1
2
3
4
5
6
7
8
9
interface State {
void insertQuarter();

void ejectQuarter();

void turnCrank();

void dispense();
}

然后每种状态都实现该接口(先看总体):

1
2
3
4
5
6
7
8
9
10
class SoldState implements State

class SoldOutState implements State

class NoQuarterState implements State

class HasQuarterState implements State

// 新需求中的“中奖状态”
class WinnerState implements State

还有新的糖果机(还没添加中奖状态):

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
class GumballMachine {
private State soldState;
private State soldOutState;
private State noQuarterState;
private State hasQuarterState;

private State state = soldState;
private int count = 0;

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

public void insertQuarter() {
state.insertQuarter();
}

public void ejectQuarter() {
state.ejectQuarter();
}

// 转动曲柄后
// 具体是否能发出糖果就需要当前状态是 hasQuarterState
public void turnCrank() {
state.turnCrank();
state.dispense();
}

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

// 不再通过 dispense 发放糖果
public void releaseBall() {
System.out.println("糖果已发出");
if (count != 0) {
count--;
}
}

// getter
public State getState() {
return state;
}

public int getCount() {
return count;
}

public State getSoldState() {
return soldState;
}

public State getSoldOutState() {
return soldOutState;
}

public State getNoQuarterState() {
return noQuarterState;
}

public State getHasQuarterState() {
return hasQuarterState;
}
}

分别实现的状态类

没投币的状态:

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
class NoQuarterState implements State {
private GumballMachine gumballMachine;

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

@Override
public void insertQuarter() {
System.out.println("投进25分钱");
gumballMachine.setState(gumballMachine.getHasQuarterState());
}

@Override
public void ejectQuarter() {
System.out.println("你还没投币呢,怎么退钱");
}

@Override
public void turnCrank() {
System.out.println("你转动了曲柄,但你还没投币呢");
}

@Override
public void dispense() {
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
28
29
class HasQuarterState implements State {
private GumballMachine gumballMachine;

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

@Override
public void insertQuarter() {
System.out.println("你已经投过25分钱了,请不要重复投币");
}

@Override
public void ejectQuarter() {
System.out.println("已退还25分钱");
gumballMachine.setState(gumballMachine.getNoQuarterState());
}

@Override
public void turnCrank() {
System.out.println("你转动了曲柄");
gumballMachine.setState(gumballMachine.getSoldState());
}

@Override
public void dispense() {
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
28
29
30
31
32
33
class SoldState implements State {
private GumballMachine gumballMachine;

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

@Override
public void insertQuarter() {
System.out.println("请稍等,正在出糖果");
}

@Override
public void ejectQuarter() {
System.out.println("你已经转动曲柄了,无法退钱");
}

@Override
public void turnCrank() {
System.out.println("转几次都只能拿一次");
}

@Override
public void dispense() {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
System.out.println("已经没有糖果喽");
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
class SoldOutState implements State {
private GumballMachine gumballMachine;

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

@Override
public void insertQuarter() {
System.out.println("没有糖果了,不要投币");
}

@Override
public void ejectQuarter() {
System.out.println("没有糖果时无法投币,不要骗钱");
}

@Override
public void turnCrank() {
System.out.println("你转动了曲柄,但是已经没有糖果了");
}

@Override
public void dispense() {
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class WinnerState implements State {
private GumballMachine gumballMachine;

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

@Override
public void insertQuarter() {
System.out.println("请稍等,正在出糖果");
}

@Override
public void ejectQuarter() {
System.out.println("你已经转动曲柄了,无法退钱");
}

@Override
public void turnCrank() {
System.out.println("转几次都只能拿一次");
}

// 除了发糖外,其他信息和售出糖果的状态一样
@Override
public void dispense() {
System.out.println("恭喜!你拿到了两颗糖");
// 第一颗糖
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("已经没有糖果喽");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
}

需求还没解决完

上面的代码只是解决了状态的问题,但还没有完全满足需求,10% 的随机中奖还没写。(其实也没什么难度了)

给糖果机加上中奖状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class GumballMachine {
// ...
private State winnerState;

public GumballMachine(String location, int count) {
winnerState = new WinnerState(this);
// ...
}

public State getWinnerState() {
return winnerState;
}

// ...
}

因为只有在投了币的情况下才能转动曲柄 -> 发糖 | 中奖,所以要在投了币的状态加入这 10% 随机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class HasQuarterState implements State {
// ...

@Override
public void turnCrank() {
System.out.println("你转动了曲柄");
Random randomWinner = new Random(System.currentTimeMillis());
// 中奖
// 10次里面有一次是0,即 1/10
int winner = randomWinner.nextInt(10);
if (winner == 0 && gumballMachine.getCount() > 1) {
gumballMachine.setState(gumballMachine.getWinnerState());
} else {
gumballMachine.setState(gumballMachine.getSoldState());
}
}

// ...
Author

Zoctan

Posted on

2018-04-05

Updated on

2023-03-14

Licensed under