策略模式的孪生兄弟---状态模式
人的机缘是神奇的,认识一个人就相当于打开了一个圈子,不管这个人是否在圈子中心,而这点,会在不经意间带给我们意想不到的作用。
如果我们在编写代码的时候,遇到大量的条件判断的时候,可能会采用策略模式来优化结构,因为这时涉及到策略的选择,但有时候仔细查看下,就会发现,这些所谓的策略其实是对象的不同状态,更加明显的是,对象的某种状态也成为判断的条件。
我们还是以一个例子入手。
假设现在我们有一个饮水机,它有以下两个状态: 满桶,空桶。初始状态是满桶,容量是20。饮水机只有一个动作:press,每次press后都会使容量减1,一旦为0,则将状态设置为空桶,这时press没有水流出。
要使用状态模式,我们必须明确两个东西:状态和每个状态下执行的动作。就像是饮水机,最基本的状态就是满桶和空桶,而这两个状态下,都可能要执行倒水这个动作,也就是press。如果饮水机的容量为0,则会进入空桶的状态。
在状态模式中,因为所有的状态都要执行相应的动作,所以我们可以考虑将状态抽象出来。
状态的抽象一般有两种形式:接口和抽象类。如果所有的状态都有共同的数据域,可以使用抽象类,但如果只是单纯的执行动作,就可以使用接口。
这里我们就用接口。
public interface DispenserState { void press(); }
然后我们再定义满桶和空桶两个状态:
public class FullState implements DispenserState { @Override public void press() { System.out.println("Water is pouring!"); } } public class NullState implements DispenserState { @Override public void press() { System.out.println("There is not water poured!"); } }
接着我们再实现饮水机:
public class WaterDispenser { private static int capacity = 20; private static DispenserState dispenserState; public WaterDispenser(DispenserState state) { dispenserState = state; } private static void setState(DispenserState state) { dispenserState = state; } public DispenserState getState() { return dispenserState; } public void press() { capacity--; if (capacity <= 0) { setState(new NullState()); } dispenserState.press(); } }
接着我们再进行测试:
public class Test { public static void main(String[] args) { WaterDispenser dispenser = new WaterDispenser(new FullState()); for (int i = 0; i < 100; ++i) { dispenser.press(); } } }
这是一个非常简单的应用场景:我们不断的press,饮水机里的水会越来越少,从满桶状态变成空桶状态。
如果我们不使用状态模式,也可以解决这个问题:
public class WaterDispenser { private static int capacity = 20; public void press() { capacity--; if (capacity <= 0) { System.out.println("There is not water poured!"); } else { System.out.println("Water is pouring!"); } } }
这样确实是更加简单,不需要有多余的接口和一系列的类,但这里的情况只有两种,如果是三种,五种,甚至更多,几十种,那我们到底需要多么可怕的if...else子句啊!而且,要是条件中再嵌套条件,简直就是难以想象!!
状态模式的好处就是将我们从这个复杂的嵌套条件中脱离出来,但状态模式的坏处也是非常明显:需要管理一系列的状态类。
状态模式的意图就是让每一个状态对修改关闭,允许对象在内部状态改变时改变它的行为,使对象看起来好像修改了它的类。
让每一个状态对修改关闭,也就是让状态类来改变状态,即封装,我们只能通过向状态类发送消息来改变状态,至于怎么改变,则完全隐藏起来。
允许对象在内部状态改变时改变它的行为,使对象看起来好像修改了它的类。其实,对象本身拥有一个状态类的对象集合,只要符合状态改变的条件,就可以将表示状态的状态类改变成下个状态类。从具体的代码来看,所谓的状态类的对象集合,其实就是我们的对象拥有一个状态类的引用,该引用可以指向任何状态类的对象集合的具体引用。对象本身一般都会有一个setState()方法,可以将状态修改成另一个状态。
至于这个对象,我们可以给它一个专门的名词:Context,也就是上下文类。
上下文是一个难以理解的词眼,放在具体的应用场景中更加直白点。
这里我们的饮水机就是一个Context,它将自己的行为委托给状态对象执行,像是press()方法,就交给具体的状态对象state的press()方法执行。
上下文类的明显特点就是拥有一个引用,然后通过该引用调用相应的方法。
我们是如何区分不同的状态?就是通过每个状态不同的行为来区分,所以不同的状态都有相同的动作,但是这个动作的执行结果是不同的,而且执行结果还有可能会修改当前的状态。为了体现多态,摆脱对具体状态类的依赖,使用Context可以动态的替换状态类,就像策略模式一样。
状态的转换不一定是放在Context中,有时候状态类本身也会自动切换状态。当状态的转换是固定的,像是这里的容量为0,就变成空桶状态,我们可以在Context中完成转换,但如果转换是动态的,也就是没有固定的判断条件,像是一完成就自动切换,我们可以放在状态类中。
但将状态的切换放在状态类中,会让状态类间产生依赖,而且状态类还需要拥有Context类的引用才能切换状态,也就是采用观察者模式的方法来通知Context类更新状态。所以,是否要这样做,就看具体的编程环境以及我们的经验了。
程序员的经验是非常重要的,它决定我们是否可以达到更高的境界,新手是在积累经验,等累积到一定的程度,编程的时候就基本靠经验的驱动了,什么是安全的代码,什么样的代码扩展性好,都会在不知不觉间体现出来,不需要特意从头脑中挖出来。
状态对象也是可以共享的,但前提就是状态对象不能持有它们自己的内部状态,否则无法保证另一个线程得到的对象是正确的状态。所以,我们如果想要共享状态,需要把每个状态都指定到静态的实例变量中。
状态模式的意图其实非常简单:将与状态有关的处理逻辑分散到代表对象状态的各个类中,但我们的Context类必须拥有这些状态对象集合的引用,这也就引出一个问题:实例化Context类对象会导致状态类对象集合初始化方面的问题,也就是对象的依赖问题。
为了尽可能减少这方面的依赖,我们的Context类通常拥有的只是一个状态类的抽象引用,然后设置一个setter以便动态的更改状态类对象。
如果不是使用对象,而是采用常量的方法:
private static final int FULL_STATE = 0; private static final int NULL_STATE = 1;
也可以使用enum将它们封装起来:
enum STATE{ NULL_STATE, FULL_STATE};
采用这种做法是因为我们需要根据当前的状态自动跳转到下一个状态,比如说,饮水机可以在空桶的状态自动加水:
if(state == NULL_STATE){ capacity++; if(capacity == 20){ state = FULL_STATE; } }
这种做法非茶馆常见,因为我们只是想要有一个状态的判断和切换的简单动作,并不需要特意创建一个对象,因为这些状态只是单纯的标识,没有任何的职责。但程序中出现过多的静态变量总是让人觉得这个程序的设计不具有良好的弹性,如果可以从这些状态中提取出抽象,可以考虑使用状态模式来优化我们的代码结构。
之所以说状态模式是策略模式的孪生兄弟,是因为它们的UML图是一样的,但意图却完全不一样,策略模式是让用户指定更换的策略算法,而状态模式是状态在满足一定条件下的自动更换,用户无法指定状态,最多只能设置初始状态。