策略模式的双胞胎:状态模式
Simple Demo
假设我有一部iPhoneX,又非常喜欢玩游戏,那么我这部破手机主要存在两种状态:待机和游戏中。
此时手机的状态图非常简单:
将这个状态图转换为代码:
每一个状态用不同的整数代表,将每一个动作整合成方法,每一个动作都可能造成状态的转换。
public class MyiPhoneX { public final static int STANDBY = 0; // 待机状态 public final static int PLAYING = 1; // 游戏进行中状态 private int state = STANDBY; // 持有当前状态的实例变量 public MyiPhoneX() { } public void startGame() { // 打开游戏 if (state == STANDBY) { System.out.println("Game is loading..."); state = PLAYING; } else if (state == PLAYING) { System.out.println("Game is already in progress!");// 已存在该游戏进程 } } public void exitGame() { // 结束游戏 if (state == STANDBY) { System.out.println("There is no game process!");// 不存在游戏进程,这是此状态的一个不恰当动作 } else if (state == PLAYING) { System.out.println("Game is exiting..."); state = STANDBY; } } }
测试代码:
public class MyiPhoneXTest { public static void main(String[] args) { MyiPhoneX iPhoneX = new MyiPhoneX(); iPhoneX.startGame(); iPhoneX.exitGame(); iPhoneX.exitGame(); iPhoneX.startGame(); iPhoneX.exitGame(); } }
Game is loading... Game is exiting... There is no game process! Game is loading... Game is exiting...
更改需求
但存在一种特殊情况:遇上了某款“渣渣游戏”,其在响应网络方面的功能没有很完善,我的iPhoneX在加载这款游戏的过程中某个瞬间网络状况不太好,进入了“游戏无限加载”状态,最后只能选择退出游戏后再重新尝试打开游戏。
下面是加入“无限加载状态”的手机状态图:
如果在第原版本的代码进行维护, 需要作如下修改:
可这样的代码不易于扩展,我的iPhoneX每新增一个状态,需要MyiPhoneX类做出的改变地方可能会更多,这样的设计不符合“对修改关闭,对扩展开放”的原则、也不够面向对象。
而且这还只是一个只有两三个状态的简单例子,如果是状态及操作方法较多的程序,按照以上的设计,代码臃肿混乱,每新增一个状态,需要在每个操作方法里都新增一个 else if 判断,所有的代码维护都堆积在一个类中。
新的设计
为了更容易的维护和修改程序,我们需要对原代码重新设计:
我们将每个状态的行为都放在各自的类中(将会改变的那部分封装起来),那么每个状态类只要实现它自己的动作就可以了,对iPhoneX执行的动作只需要委托给当前状态的状态对象来具体执行就好了。我们可以这么做:
① 先定义一个State接口,iPhoneX可以执行的每一个动作都有对应的方法在这个接口内。(面向接口编程)
② iPhoneX的每个状态都有一个对应的实现了State接口的具体状态类。
③ 对iPhoneX执行动作委托给具体状态类来执行。
状态接口和其具体实现类的类图如下:
实际代码:
public interface State { void startGame(); void exitGame(); }
具体状态实现类主要做的是实现适合所在的这个状态的具体动作。
public class Standby implements State { //“待机”状态 private MyiPhoneX iPhoneX; public Standby(MyiPhoneX iPhoneX) { this.iPhoneX = iPhoneX; } @Override public void startGame() { System.out.println("Game is loading..."); // 游戏正在加载 iPhoneX.setState(iPhoneX.getPlaying()); // iPhone修改为“游戏中状态” } @Override public void exitGame() { System.out.println("There is no game process!");// 不存在该游戏进程,这是此状态的一个不恰当动作 } }
public class Playing implements State { //“游戏中”状态 private MyiPhoneX iPhoneX; public Playing(MyiPhoneX iPhoneX) { this.iPhoneX = iPhoneX; } @Override public void startGame() { System.out.println("Game is already in progress!");// 已存在该游戏进程 } @Override public void exitGame() { System.out.println("Game is exiting..."); // 退出游戏中 iPhoneX.setState(iPhoneX.getStandby()); // iPhone修改为“待机状态” } }
我的iPhoneX实体类:
public class MyiPhoneX { private State standby; private State playing; private State state ; // 持有当前状态的实例变量 public MyiPhoneX() { standby = new Standby(this); playing = new Playing(this); state = standby; } public void startGame() { // 打开游戏 state.startGame(); } public void exitGame() { // 结束游戏 state.exitGame(); } public void setState(State state) { // 用于修改手机当前状态 this.state = state; } public State getStandby() { return standby; } public State getPlaying() { return playing; } }
实现需求
将新的“游戏无限加载”状态加上,代码修改如下:
增加遇到渣渣游戏的“游戏无限加载”状态类:
public class RubbishGame implements State { //“游戏无限加载”状态 private MyiPhoneX iPhoneX; public RubbishGame(MyiPhoneX iPhoneX) { this.iPhoneX = iPhoneX; } @Override public void startGame() { System.out.println("Game is already in progress!");// 已存在该游戏进程 } @Override public void exitGame() { System.out.println("Game is exiting..."); // 退出游戏中 iPhoneX.setState(iPhoneX.getStandby()); // iPhone修改为“待机状态” } }
在MyiPhoneX类中添加引用“游戏无限加载”状态的实例变量:
public class MyiPhoneX { private State standby; private State playing; private State rubbishGame; //添加遇到渣渣游戏的“游戏无限加载”状态 private State state; // 持有当前状态的实例变量 public MyiPhoneX() { standby = new Standby(this); playing = new Playing(this); rubbishGame = new RubbishGame(this); //初始化“游戏无限加载”状态 state = standby; } public void startGame() { // 打开游戏 state.startGame(); } public void exitGame() { // 结束游戏 state.exitGame(); } public void setState(State state) { // 用于修改手机当前状态 this.state = state; } public State getStandby() { return standby; } public State getPlaying() { return playing; } public State getRubbishGame() { //提供获取“游戏无限加载”状态的方法 return rubbishGame; } }
修改“待机”状态类,我们用随机数代表遇到“渣渣游戏”的机会,增加从“待机”到“游戏无限加载”状态的转换的代码:
public class Standby implements State { private MyiPhoneX iPhoneX; private Random randomRubbish = new Random(); //用于产生随机数 public Standby(MyiPhoneX iPhoneX) { this.iPhoneX = iPhoneX; } @Override public void startGame() { int rubbish = randomRubbish.nextInt(10); if (rubbish <= 1) { System.out.println("Rubbish Game!"); // 游戏正在加载 iPhoneX.setState(iPhoneX.getRubbishGame()); // iPhone修改为“游戏中状态” } else { System.out.println("Game is loading..."); // 游戏正在加载 iPhoneX.setState(iPhoneX.getPlaying()); // iPhone修改为“游戏中状态” } } @Override public void exitGame() { System.out.println("There is no game process!");// 不存在该游戏进程,这是此状态的一个不恰当动作 } }
测试代码:
public class MyiPhoneXTest { public static void main(String[] args) { MyiPhoneX iPhoneX = new MyiPhoneX(); iPhoneX.startGame(); iPhoneX.exitGame(); iPhoneX.startGame(); iPhoneX.exitGame(); iPhoneX.startGame(); iPhoneX.exitGame(); iPhoneX.startGame(); iPhoneX.exitGame(); } }
Rubbish Game! Game is exiting... Game is loading... Game is exiting... Rubbish Game! Game is exiting... Game is loading... Game is exiting...
这样就为我的iPhoneX增加了一个新的状态,并实现了这个新的状态。要做的只是新增这个状态类及增加能够转换到这个状态的代码。
相对最早的代码版本,删除了容易产生问题的if语句,日后更好维护,让大多数状态类“对修改关闭”,让MyiPhoneX“对扩展开放”,而且这样的代码结构,对应起相关的状态图也更容易阅读和理解。
在这个比较简单的例子好处看起来可能不是很明显;假如是在多个状态的程序,我们新增一个状态,只需新增这个状态的实体类、在主体类中加入这个新状态类的实例引用、在能够转换到这个状态的其他状态类加入转换的逻辑代码。
状态模式:
以上的新设计采用了状态模式,状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
这个模式将状态封装成独立的类,并将动作委托到代表当前状态的对象,我的iPhoneX在“待机”和“游戏中”两种不同状态时,如果执行“开始游戏”的动作,就会得到不同的具体行为(提示已开启游戏和启动游戏)。
我们使用组合通过简单引用不同的状态对象来造成类改变的假象(执行一个动作时,当前的状态对象的具体动作里可能会转换内部的状态,而客户的视角是主体对象自己发生了改变)。
状态模式的类图:
我们可以发现,它和策略模式的类图是一样的。
两个模式的差别在于它们的“意图”不同。
①对状态模式而言
将具体行为封装在状态类中,Context的行为委托给当前具体状态对象来执行,执行具体行为后可能会改变当前状态,这样Context执行的具体行为也在改变(每个状态类对每种行为都有各自的具体实现)。而使用Context的客户对于状态对象却是陌生的、甚至是未知的。这个方案可以去掉原本存在Context内的许多的条件判断,通过Context对象内简单的改变状态对象来改变行为。
②对策略模式而言
客户对于策略对象是很熟悉的,通常由客户主动选择Context需要组合的策略对象,而且虽然策略模式可以在运行时改变策略,但对于某个Context对象来说,通常只有一个最适当的策略对象。一般用来作为除了继承之外的一种弹性替代方案,通过组合不同对象来改变行为。
参考书籍:《Head First 设计模式》