十、状态模式(State Pattern)《HeadFirst设计模式》读书笔记
假设有一个需求:要求实现一个糖果机的功能,糖果机的功能如下,用一个状态图来表示:
上图描述了糖果机在不同状态下可能会执行的操作和状态间的转换关系,图中每个圆圈代表一种状态。
下面我们按照常规的思路来实现一个糖果机,创建一个糖果机类,用int常量分别代表四种状态,另外定义int变量state代表当前状态,定义int变量count代表糖果机当前的糖果数目,还有其它状态转换时需要的一些方法。
public class GumballMachine { //糖果机的四种状态 private static final int SOLD_OUT = 0; private static final int NO_25 = 1; private static final int HAS_25 = 2; private static final int SOLD = 3; //当前状态,初始值为售罄 private int state = SOLD_OUT; //当前糖果数量 private int count = 0; //构造方法,传入初始糖果数量 public GumballMachine(int count) { this.count = count; } //投入25的方法,需要判断当前状态,且投入成功要转换到HAS_25的状态 public void insert25(int state){ if (state == SOLD_OUT) { System.out.println("糖果售罄了,目前不能购买"); return; } if (state == NO_25) { this.state = HAS_25; System.out.println("投币成功"); return; } if (state == HAS_25) { System.out.println("已经投过币了"); return; } if (state == SOLD) { System.out.println("已经投过币了,正在准备发放糖果,请稍后再投币"); } } //同理其它方法省略... }
可以看到,上面的代码并不容易扩展,各种if语句,比如需要增加状态时,每个方法又要加一个判断,这就需要状态模式登场了。
经过之前各种模式的学习,其实不同的设计模式也只是应用场景不同,但其实每个模式的原理都是通过一些类似的手法去实现的。如果我们自己设计一个状态模式,首先可以想到要将状态抽象出一个接口,然后由不同的具体状态去实现状态接口,实现状态之间互相转换的方法。
首先定义一个接口MyState,在接口中有所有的抽象方法。
public interface MyState { void insert25(); void rollBack25(); void turnHandle(); void giveCandy(); }
接着实现状态接口,写出每一个具体的状态类,这里以没有25分钱的状态为例。
public class No25State implements MyState { //具体的状态内部维护了MyGumballMachine对象,是为了调用它的setState方法,和get下一个状态来改变状态 private MyGumballMachine myGumballMachine; public No25State(MyGumballMachine myGumballMachine) { this.myGumballMachine = myGumballMachine; } @Override public void insert25() { //投币成功,将状态设置为有25分钱状态 myGumballMachine.setState(myGumballMachine.getHas25State()); System.out.println("投币成功"); } @Override public void rollBack25() { System.out.println("请先投币"); } @Override public void turnHandle() { System.out.println("请先投币"); } @Override public void giveCandy() { System.out.println("请先投币"); } }
可以看到具体的状态类内部维护了一个糖果机的变量,这么做的原因是想更方便的在具体的状态类中进行状态的转变,上面例子中就调用了糖果机的getHas25state获取了下一个要转变的状态,并调用了setState方法完成了状态的转换。下面来具体看一下糖果机的具体实现。
public class MyGumballMachine { //所有的状态 private MyState no25State; private MyState has25State; private MyState soldState; private MyState soldOutState; //构造方法,初始化所有的状态,如果糖果数>0,将当前状态设置为no25State public MyGumballMachine(int initCount) { this.count = initCount; this.no25State = new No25State(this); this.has25State = new Has25State(this); this.soldState = new SoldState(this); this.soldOutState = new SoldOutState(this); if (initCount > 0) { this.state = no25State; } } //当前状态 private MyState state; //当前糖果数量 private int count; //将具体的动作委托给当前的状态对象去执行 public void insert25(){ this.state.insert25(); } public void rollBack25(){ this.state.rollBack25(); } public void turnHandle(){ this.state.turnHandle(); } public void giveCandy(){ this.state.giveCandy(); } //当前状态的set方法,允许状态对象将糖果机转换到不同的状态 void setState(MyState state) { this.state = state; } //一些get方法... MyState getHas25State() { return has25State; } }
可以看到状态模式通过定义不同的状态类免去了复杂的if-else语句,在扩展时也可以直接实现状态接口方便地实现新的功能,同时将具体动作委托给状态对象去执行,当状态发生改变时,这些行为也会相应改变。
状态模式的定义:允许对象在内部状态改变时改变它的行为,对象看起来好像给修改了它的类。
也就是说,在糖果机内部状态改变了的时候,虽然还是调用糖果机相同的方法,但是却有不同的执行结果,这是因为糖果机委托给了具体的状态对象去执行。
下面是状态模式的类图,图中的Context就对应例子中的糖果机,request()方法内部会委托给状态对象的handle()方法执行。
关于状态模式的小例子,再做一个小总结:
1.例子中是在具体的状态类中去改变Context状态的,实际上在Context和具体的状态类中都可以改变状态,如果状态改变是固定的情况,一般直接在Context类中去改变状态;如果状态改变比较动态,比如需要一些条件判断,则一般在具体状态类中去改变,但这样的缺点就是不同的状态类之间会产生依赖关系,例子中也是通过调用Context的get方法来尽量减小依赖。
2.客户类不会去改变Context的具体状态,因此可以注意到例子中的setState方法是(defalt)而不是public。
3.例子中的Context类中定义了所有的状态,方便统一获取,当有很多Context实例想要共享这些变量时,也可以将状态指定为是静态的。
4.当在具体的状态中要用到Context中的方法或变量,则要在handle方法中传入Context的引用,或者像例子中的直接在具体的状态类中定义Context变量。
关于状态模式的总结:
其实状态模式和一些设计模式也很相似,比如策略模式、命令模式等,只要记住它的使用场景是不同状态互相转换,不同状态下会有不同的行为。但状态模式也有它的缺点,就是会产生很多具体的状态类,这就需要在设计时对代码的扩展性和简洁性之间进行权衡。