状态模式
本篇是《HeaderFirst设计模式》读书笔记。
状态模式定义:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
不知道你看了定义有啥感悟没,反正我看了是不知道在说啥。下面从实际例子出发
我们要制作一个糖果机。糖果机的控制器的工作如下图:
我们把这张图的每个圆圈看成一个状态,每个箭头看成状态的转换。所以根据图片,我们能找到的四个状态:没有25美分、有25美分、糖果售罄、售出糖果;改变状态的动作:投入25分钱、退回25分钱、转动曲柄、发放糖果。没一个动作都会造成糖果机状态的改变。
在这里我们使用一个通用技巧:如何对对象内的状态建模——通过创建一个实例变量来持有状态值,并在方法内书写条件代码来处理不同状态。
糖果机代码编写:
我们利用实例变量来持有当前的状态,然后需要处理所有可能发生的动作、行为和状态的转换。
public class GumballMachine {
// 每一个状态都用一个不同的整数代表
final static int SALD_OUT = 0,
NO_QUARTER = 1,
HAS_QUARTER = 2,
SOLD = 3;
// 持有当前的状态。因为糖果机一开始拆箱并安装的时候是没有糖果的,所以我们将它设为“糖果售罄”状态
int state = SALD_OUT;
// 追踪糖果机内的糖果数目
int count = 0;
public GumballMachine(int count) {
this.count = count;
// 如果糖果数量大于0,进入“没有硬币”状态
if(count > 0) state = NO_QUARTER;
}
// 投入硬币
public void insertQuarter() {
if(state == SALD_OUT) {
System.out.println("You can't insert a quarter, the machine is sold out.");
} else if(state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("you inserted a quarter");
} else if(state == HAS_QUARTER) {
System.out.println("You can't insert another quarter");
} else if(state == SOLD) {
// 如果顾客刚刚才买了糖果,就需要稍等一下,好让状态转换完毕,回复到“没有硬币”的状态
System.out.println("Please wait, we're already giving you a gumball");
}
}
// 退回硬币
public void ejectQuarter() {
if(state == SALD_OUT) {
System.out.println("You can't eject, you haven't inserted a quarter yet");
} else if(state == NO_QUARTER) {
System.out.println("You haven't inserted a quarter");
} else if(state == HAS_QUARTER) {
state = NO_QUARTER;
System.out.println("Quarter returned");
} else if(state == SOLD) {
System.out.println("Sorry, you already turned the crank");
}
}
// 转动曲柄
public void turnCrank() {
if(state == SALD_OUT) {
System.out.println("You turned, but there are no gumballs");
} else if(state == NO_QUARTER) {
System.out.println("You turned but there's no quarter");
} else if(state == HAS_QUARTER) {
// 改变状态到“售出糖果”,然后调用机器的dispense()方法。
state = SOLD;
System.out.println("You turned...");
dispense();
} else if(state == SOLD) {
System.out.println("Turning twice doesn't get you anoter gumball!");
}
}
// 发放糖果
public void dispense() {
if(state == SALD_OUT) {
System.out.println("No gumball dispensed");
} else if(state == NO_QUARTER) {
System.out.println("You need to pay first");
} else if(state == HAS_QUARTER) {
System.out.println("No gumball dispensed");
} else if(state == SOLD) {
System.out.println("A gumball comes rolling out the slot");
count --;
if(count == 0){
System.out.println("Oops, out of gumblls!");
state = SALD_OUT;
} else {
state = NO_QUARTER;
}
}
}
}
现在我们在这个糖果机的基础上加功能:10人有1人可以得到免费的糖果。加入此功能后糖果机的工作状态图如下:
如果对上面的代码进行修改,你会发现要改的东西特别多乱杂,首先要加一个新的状态,其次要在每个方法中接入一个新的条件判断来处理“赢家”状态。如果随着糖果机优化,需要加入越来越多状态的话,那么就需要对这段代码不断的修改增添,程序中bug的几率就会整大。
所以我们就要改动啦,不维护现有的代码,重写它以便于将状态对象封装在各自的类中,然后在动作发生时委托给当前的状态。
1、 首先,我们定义一个State接口。在这个接口内,糖果机的每个动作都有一个对应的方法。
2、 然后为机器中的每个状态实现状态类。这些类将负责在对应的状态下进行机器行为。
3、 最后,我们要摆脱旧的条件代码,取而代之的方式是,将动作委托到状态类。
实现状态接口:
首先,让我们创建一个State接口,所有的状态都必须实现这个接口:
/**
* 所有状态的接口。这些方法直接映射到糖果机上可能发生的动作。
* @author Administrator
*/
public interface State {
public void insertQuarter();
void ejectQuarter();
void turnCrank();
void dispense();
}
实现状态类:
我们先从NoQuarterState开始。
public class NoQuarterState implements State {
GumballMachine gumballMachine;
public NoQuarterState(com.bajie.status1.GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("you inserted a quarter");
gumballMachine.setState(gumballMachine.hasQuarterState);
}
@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");
}
}
HasQuarterState:
public class HasQuarterState implements State {
GumballMachine gumballMachine;
public HasQuarterState(com.bajie.status1.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());
}
/* (non-Javadoc)
* @see com.bajie.status1.State#turnCrank()
*/
@Override
public void turnCrank() {
System.out.println("You turned...");
gumballMachine.setState(gumballMachine.getSoleState());
}
@Override
public void dispense() {
System.out.println("No gumball dispensed");
}
}
其他状态类与此类相似。
重新改造的糖果机
public class GumballMachine {
// 糖果机的四个状态
State soldOutState,
noQuarterState,
hasQuarterState,
soleState,
winnerState;
// 跟踪糖果机的当前状态
State state = soldOutState;
// 追踪糖果机内的糖果数目
int count = 0;
public GumballMachine(int count) {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soleState = new SoldState(this);
winnerState = new WinnerState(this);
this.count = count;
// 如果糖果数量大于0,进入“没有硬币”状态
if(count > 0) state = noQuarterState;
}
// 投入硬币
public void insertQuarter() {
state.insertQuarter();
}
// 退回硬币
public void ejectQuarter() {
state.ejectQuarter();
}
/**
* 我们不需要在GumballMachine中准备一个dispense()的动作方法,因为这只是一个内部的动作;
* 用户不可以直接要求机器发送糖果。
* 但我们是在状态对象的turnCrank()方法中调用dispense()方法的。
*/
// 转动曲柄
public void turnCrank() {
state.turnCrank();
// 发放糖果
state.dispense();
}
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 getSoldOutState() {
return soldOutState;
}
public void setSoldOutState(State soldOutState) {
this.soldOutState = soldOutState;
}
public void setNoQuarterState(State noQuarterState) {
this.noQuarterState = noQuarterState;
}
public State getHasQuarterState() {
return hasQuarterState;
}
public void setHasQuarterState(State hasQuarterState) {
this.hasQuarterState = hasQuarterState;
}
public State getSoleState() {
return soleState;
}
public void setSoleState(State soleState) {
this.soleState = soleState;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public State getState() {
return state;
}
public State getNoQuarterState() {
return noQuarterState;
}
public State getWinnerState() {
return winnerState;
}
public void setWinnerState(State winnerState) {
this.winnerState = winnerState;
}
}
至此我们已经实现了状态模式,如果我们需要再加入一个状态就易如反掌了。
加入一个WinnerState状态类:
public class WinnerState implements State {
GumballMachine gumballMachine;
public WinnerState(com.bajie.status1.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 anoter gumball!");
}
@Override
public void dispense() {
System.out.println("YOU'RE A WINNER! You get two gunballs for your quarter");
gumballMachine.releaseBall();
if(gumballMachine.getCount() == 0) {
gumballMachine.setState(gumballMachine.getSoldOutState());
} else {
gumballMachine.releaseBall();
if(gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
gumballMachine.setState(gumballMachine.getSoldOutState());
System.out.println("Oops, out of gumballs!");
}
}
}
}
然后稍微改造一下HasQuarterState的代码:
import java.util.Random;
public class HasQuarterState implements State {
Random randomWinner = new Random(System.currentTimeMillis());
GumballMachine gumballMachine;
public HasQuarterState(com.bajie.status1.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());
}
/* (non-Javadoc)
* @see com.bajie.status1.State#turnCrank()
*/
@Override
public void turnCrank() {
System.out.println("You turned...");
int random = randomWinner.nextInt(10);
if(random == 0 && gumballMachine.getCount() > 1) {
gumballMachine.setState(gumballMachine.winnerState);
} else {
gumballMachine.setState(gumballMachine.getSoleState());
}
}
@Override
public void dispense() {
System.out.println("No gumball dispensed");
}
}
下面运行我们的糖果机:
public class Main {
public static void main(String[] args) {
GumballMachine gumballMachine = new GumballMachine(5);
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
}
}
和之前的糖果机相比,我们做了一下几点:
1、 将每个状态的行为局部化到它自己的类中。
2、 将容易产生问题的if语句删除,以方便日后的维护
3、 让每一个状态“对修改关闭”,让糖果机“对扩展开放”,因为可以加入新的状态类。
4、 创建一个新的代码基和类结构,这更能映射万能糖果机的图,而且更容易阅读和理解。
接下来给出状态模式的类图:
最后总结下状态模式的要点:
1、 状态模式允许一个对象基于内部状态而拥有不同的行为。
2、 状态模式用类代表状态
3、 Context会将行为委托给当前状态对象
4、 通过将每个状态封装进一个类,我们把以后需要做的任何改变局部化了
5、 状态模式通常会用行为或算法来配置Context类
6、 状态模式允许Context随着状态的改变而改变行为
7、 状态转换可以由State类或Context类控制
8、 使用状态模式通常会导致设计中类的数目大量增加
9、 状态类可以被多个Context实例共享。