状态模式State Pattern
状态模式解决什么问题
在《Java开发手册-华山版》的编程规约-控制语句中有讲:超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现。
当控制一个对象状态转换的条件表达式过于复杂时,把相关“判断逻辑”提取出来,用各个不同的类进行表示,系统处于哪种情况。直接使用相应的状态类进行处理,这样能把原来复杂的逻辑判断简单化,消除了 if-else、switch-case 等冗余语句。这样的代码更有层次性,并且具备良好的扩展力。
什么是状态模式
定义:
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。 (如:人都有高兴和伤心的时候,不同的情绪有不同的行为。当然外界也会影响其情绪变化。)
类结构图:
状态模式包含以下主要角色。
- 环境类(Context) :也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
- 抽象状态(State) :定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
- 具体状态(Concrete State) :实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。
状态模式的优缺点
优点:
- 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
状态模式简单例子
假设我们在开发一款糖果机游戏。
图中可以发现,它有4个状态(没有硬币,已经投币,糖果售馨,售出糖果),以及有4个动作(投入硬币,退回硬币,转动曲柄,派发糖果)
public class CandyMachine {
final static int SOLD_OUT = 0; // 售馨状态
final static int NO_COIN = 1; // 没硬币状态
final static int HAS_COIN = 2; // 已投币状态
final static int SOLD = 3; // 售出状态
private int state = SOLD_OUT;
private int count = 0;
public CandyMachine(int count) {
this.count = count;
if (count > 0) {
state = NO_COIN;
}
}
/**
* 投入硬币
*/
public void insertCoin() {
switch (state) {
case SOLD_OUT:
System.out.println("糖果已售馨,无法投币");
break;
case NO_COIN:
state = HAS_COIN;
System.out.println("您已经投币,请转动曲柄");
break;
case HAS_COIN:
System.out.println("您已经投币,无法继续投币");
break;
case SOLD:
System.out.println("糖果正在发放中,请稍后");
break;
}
}
/**
* 退回硬币
*/
public void ejectCoin() {
switch (state) {
case SOLD_OUT:
System.out.println("您还没有投币");
break;
case NO_COIN:
System.out.println("您没有投过币");
break;
case HAS_COIN:
System.out.println("退回硬币");
state = NO_COIN;
break;
case SOLD:
System.out.println("您已经转动过曲柄,无法退回硬币");
break;
}
}
/**
* 转动曲柄
*/
public void turnCrank() {
switch (state) {
case SOLD_OUT:
System.out.println("糖果已售馨");
break;
case NO_COIN:
System.out.println("您没有投过币,无法转动曲柄");
break;
case HAS_COIN:
System.out.println("正在发放糖果");
state = SOLD;
dispense();
break;
case SOLD:
System.out.println("多次转动曲柄无效");
break;
}
}
/**
* 派发糖果
*/
private void dispense() {
switch (state) {
case SOLD_OUT:
System.out.println("程序错误");
break;
case NO_COIN:
System.out.println("您得先投币");
break;
case HAS_COIN:
System.out.println("程序错误");
break;
case SOLD:
count = count - 1;
System.out.println("您得到了一个糖果");
if (count > 0) {
state = NO_COIN;
} else {
state = SOLD_OUT;
}
break;
}
}
public void printState() {
switch (state) {
case SOLD_OUT:
System.out.println("***SOLD_OUT***");
break;
case NO_COIN:
System.out.println("***NO_QUARTER***");
break;
case HAS_COIN:
System.out.println("***HAS_QUARTER***");
break;
case SOLD:
System.out.println("***SOLD***");
break;
}
}
}
需求变动,在当前的逻辑上,加入有10%的概率可以拿到2粒糖果。这样便会多一个状态"中奖状态"。
在当前的逻辑上修改,首先,必须加上一个新的状态“WINNER”,然后给insertCoin ejectCoin turnCrank三个方法增加WINNER的判断,最后在turnCrank编写中奖状态的逻辑。(明显违反了,对扩展开放,对修改关闭的开闭原则,也不符合面对对象的设计。)
状态模式实现
为了让代码更容易维护,我们需要将当前的设计改成状态模式,重构需做的事情有:
- 定义一个State接口。在接口内,糖果机的每个动作都有一个对应的方法。
- 实现糖果的每个状态。将糖果机的动作委托到状态类。
public interface State {
//插入硬币
public void insertCoin();
//退回硬币
public void ejectCoin();
//转动曲柄
public void turnCrank();
//派发糖果
public void dispense();
}
SoldOutState、HasCoinState、NoCoinState、SoldState、 WinnerState 各状态实现State接口
public class HasCoinState implements State {
//Random randomWinner = new Random(System.currentTimeMillis());
private final CandyMachine candyMachine;
public HasCoinState(CandyMachine candyMachine) {
this.candyMachine = candyMachine;
}
@Override
public void insertCoin() {
System.out.println("您已经投币,无法继续投币,请转动曲柄");
}
@Override
public void ejectCoin() {
System.out.println("退回硬币");
candyMachine.setState(candyMachine.noCoinState);
}
@Override
public void turnCrank() {
System.out.println("正在发放糖果");
// 加入WinnerState需要改变HasCoinState.turnCrank的逻辑。这里暴露了状态模式的一个缺点
int winner = randomWinner.nextInt(10);
if (winner == 0 && candyMachine.getCount() > 1)
candyMachine.setState(candyMachine.winnerState);
else
// 没有WinnerState时,只需切换soldState
candyMachine.setState(candyMachine.soldState);
}
@Override
public void dispense() {
System.out.println("没有糖果可派发");
}
}
重构CandyMachine。并加入新的状态,中奖状态。
public class CandyMachine {
final State soldOutState; // 售馨状态
final State noCoinState; // 没硬币状态
final State hasCoinState; // 已投币状态
final State soldState; // 售出状态
final State winnerState; // 中奖状态
private State state = null;
private int count = 0;
public CandyMachine(int count) {
soldOutState = new SoldOutState(this);
noCoinState = new NoCoinState(this);
hasCoinState = new HasCoinState(this);
soldState = new SoldState(this);
winnerState = new WinnerState(this);
this.count = count;
if (count > 0) {
state = noCoinState;
}
}
/**
* 投入硬币
*/
public void insertCoin() {
state.insertCoin();
}
/**
* 退回硬币
*/
public void ejectCoin() {
state.ejectCoin();
}
/**
* 转动曲柄
*/
public void turnCrank() {
state.turnCrank();
state.dispense();
}
/**
* 派发糖果
*/
void releaseCandy() {
if (count != 0) {
count = count - 1;
System.out.println("您得到了一个糖果");
}
}
void setState(State state) {
this.state = state;
}
public int getCount() {
return count;
}
public State getState() {
return state;
}
public void printState() {
System.out.println(state.toString());
}
}
完成重构后,糖果机即使要加入一个新的状态,只需实例状态类。剩下的逻辑都委托给了状态类。
再理解状态模式定义的涵义
通过上面的例子在来理解状态类的定义。
"允许对象在内部状态发生改变时改变它的行为",因为这个模式将状态封装成为独立的类,并将动作委托到代表当前的对象,我们知道行为会随着内部状态而变化。糖果机的例子中,当糖果机在NoCoinState或HasCoinState两种不同的状态是,当投入硬币,就会得到不同的行为(机器接受硬币和机器拒绝硬币)。
"对象看起来好像修改了它的类",从客户的视角来看;如果说你使用的对象能够完全改变它的行为,那么就会觉得,这个对象实际上是从别的类实例化而来的。然而,实际上,我们是在使用组合通过简单引用不同的“状态对象”来造成类改变的假象。
为什么需要WinnerState,为什么不直接在SoldState中发放两颗糖果?
两个状态几乎一样,唯一的区别在于,WinnerState会派发两颗糖果。将派发两颗糖果的代码放到SoldState中,当然是可以的。这么做也会有缺点,因为这样等于是将两个状态用一个状态类来代表。这样做是牺牲了状态类的清晰易懂来减少一些冗余代码。但也违背了一个类,一个责任的原则。将WinnerState的责任放进SoldState中,等于是让SoldState具有两个责任。那么促销方案结束之后或者赢家的机率改变之后,又该怎么办呢?