前言
我们遇到状态机模型,常常会理不清“状态”和“行为”的关系。状态模式就是专门解决这个应用场景的,它通过改变对象内部的状态来帮助对象控制自己的行为。
正文
1、先来看一个案例
我们选取《HeadFirst 设计模式》中的糖果售卖机案例。这就是常见的状态机模型。
首先我们找出所有的状态:上图中的4个圆圈。于是我们用4个常量来表示每个状态:
final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
int state = SOLD_OUT; // 初始状态
然后我们将系统中可以发生的动作整理出来,每个动作都对应一个方法。下面这个方法就是投币动作对应的方法。方法里面对每一个可能出现的状态展示适当的行为。
public void insertQuarter() {
if (state == HAS_QUARTER) {
sout("不能再投币了");
} else if (state == SOLD_OUT) {
sout("卖完了");
} else if (state == SOLD) {
sout("已售出");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
sout("已投币");
}
}
public void ejectQuarter() {
if (state == HAS_QUARTER) {
...
} else if (state == SOLD_OUT) {
...
} else if (state == SOLD) {
...
} else if (state == NO_QUARTER) {
...
}
}
public void turnCrank() {
if (state == HAS_QUARTER) {
...
} else if (state == SOLD_OUT) {
...
} else if (state == SOLD) {
...
} else if (state == NO_QUARTER) {
...
}
}
public void dispense() {
if (state == HAS_QUARTER) {
...
} else if (state == SOLD_OUT) {
...
} else if (state == SOLD) {
...
} else if (state == NO_QUARTER) {
...
}
}
其他的动作方法类似,里面都有一大堆if else代码。当我们费尽九牛二虎之力写完后,要加入新的需求:当转动糖果机摇杆,有10%的几率掉下来2颗糖果。这种情况下完全失控了,每一个动作方法都要跟着修改,改出BUG的几率会大大增加。
2、如何优化
仍然遵循“封装变化”的原则,我们将每个状态的行为都放在各自的类中,那么每个状态只需要实现它自己的动作就可以了。这样,糖果机只需要将动作委托给当前的状态对象。
- 首先,我们定义一个State接口,在这个接口中,糖果机的每个动作都有一个对应的方法;
- 然后,为机器中的每个状态实现一个状态类,这些类将负责在对应状态下进行机器的行为;
- 最后,我们将糖果机的动作委托给状态类。
我们尝试实现其中一个状态类,我们要做的是实现当前状态下恰当的行为:
public class NoQuarterState implements State {
Machine machine;
// 构造器拿到糖果机的引用
public NoQuarterState(Machine machine) {
this.machine = machine;
}
// 当前状态下触发动作的结果
public void insertQuarter() {
sout("已投币");
// 触发糖果机改变状态
machine.setState(machine.getHasQuarterState());
}
public void ejectQuarter() {
sout("未投币");
}
public void turnCrank() {
sout("未投币");
}
public void dispense() {
sout("未投币");
}
}
于是我们重新改造糖果机:
public class Machine {
// 常量替换成4个状态类
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State state = soldOutState; // 当前状态
// 构造方法初始化每种状态
public Machine() {
soldOutState = new SoldOutState(this);
noQuarterState= new NoQuarterState(this);
hasQuarterState= new HasQuarterState(this);
soldState = new SoldState(this);
}
// 糖果机的动作均委托给当前状态类的动作方法
public void insertQuarter() {
soldState.insertQuarter();
}
public void ejectQuarter() {
soldState.ejectQuarter();
}
public void turnCrank() {
soldState.turnCrank();
}
public void dispense() {
soldState.dispense();
}
}
3、定义状态模式
上面已经实现了状态模式,我们来对其进行一个定义:
状态模式允许对象在内部状态改变时改变它自身的行为,对象看起来好像修改了它的类。
- 这个模式将状态封装成独立的类,并将动作委托给当前状态对象
- 从用户的视角来看,如果使用的对象能否完全改变自己的行为,那么这个对象应该是从别的类实例化而来的。然而,状态模式仅通过组合不同的状态对象来造成改变类行为的假象。
我们来定义下状态模式的类图:
4、状态模式 vs 策略模式
两个模式的类图很相似,但是两个模式的行为不一样:策略模式替换算法是由用户自己控制的,状态模式替换算法是系统内部自行控制的。
总结
状态模式针对状态机场景有很好的模型总结,我们遇到状态机模型,都要首先考虑状态模式。