(二十一)状态模式详解(DOTA版)
作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可。
本次LZ给各位介绍状态模式,之前在写设计模式的时候,引入了一些小故事,二十章职责连模式是故事版的最后一篇,之后还剩余四个设计模式,LZ会依照原生的方式去解释这几个设计模式,特别是原型模式和解释器模式,会包含一些其它的内容。
好了,接下来,我们先来看看状态模式的定义吧。
定义:(源于Design Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
上述是百度百科中对状态模式的定义,定义很简单,只有一句话,请各位形象的去理解这句话,它说当状态改变时,这个对象的行为也会变,而看起来就像是这个类改变了一样。
这正是应验了我们那句话,有些人一旦发生过什么事以后,就像变了个人似的,这句话其实与状态模式有异曲同工之妙。
我们仔细体会一下定义当中的要点。
1、有一个对象,它是有状态的。
2、这个对象在状态不同的时候,行为不一样。
3、这些状态是可以切换的,而非毫无关系。
前两点比较好理解,第3点有时候容易给人比较迷惑的感觉,什么叫这些状态是可以切换的,而非毫无关系?
举个例子,比如一个人的状态,可以有很多,像生病和健康,这是两个状态,这是有关系并且可以转换的两个状态。再比如,睡觉、上班、休息,这也算是一组状态,这三个状态也是有关系的并且可以互相转换。
那如果把生病和休息这两个状态放在一起,就显得毫无意义了。所以这些状态应该是一组相关并且可互相切换的状态。
下面我们来看看状态模式的类图。
类图中包含三个角色。
Context:它就是那个含有状态的对象,它可以处理一些请求,这些请求最终产生的响应会与状态相关。
State:状态接口,它定义了每一个状态的行为集合,这些行为会在Context中得以使用。
ConcreteState:具体状态,实现相关行为的具体状态类。
如果针对刚才对于人的状态的例子来分析,那么人(Person)就是Context,状态接口依然是状态接口,而具体的状态类,则可以是睡觉,上班,休息,这一系列状态。
不过LZ也看过不少状态模式的文章和帖子,包括《大话设计模式》当中,都举的是有关人的状态的例子,所以这里给大家换个口味,我们换一个例子。
我们来试着写一个DOTA的例子,最近貌似跟DOTA干上了,不为其他,就因为DOTA伴随了LZ四年的大学时光。
玩过的朋友都知道,DOTA里的英雄有很多状态,比如正常,眩晕,加速,减速等等。相信就算没有玩过DOTA的朋友们,在其它游戏里也能见到类似的情况。那么假设我们的DOTA没有使用状态模式,则我们的英雄类会非常复杂和难以维护,我们来看下,原始版的英雄类是怎样的。
package com.state; //英雄类 public class Hero { public static final int COMMON = 1;//正常状态 public static final int SPEED_UP = 2;//加速状态 public static final int SPEED_DOWN = 3;//减速状态 public static final int SWIM = 4;//眩晕状态 private int state = COMMON;//默认是正常状态 private Thread runThread;//跑动线程 //设置状态 public void setState(int state) { this.state = state; } //停止跑动 public void stopRun() { if (isRunning()) runThread.interrupt(); System.out.println("--------------停止跑动---------------"); } //开始跑动 public void startRun() { if (isRunning()) { return; } final Hero hero = this; runThread = new Thread(new Runnable() { public void run() { while (!runThread.isInterrupted()) { try { hero.run(); } catch (InterruptedException e) { break; } } } }); System.out.println("--------------开始跑动---------------"); runThread.start(); } private boolean isRunning(){ return runThread != null && !runThread.isInterrupted(); } //英雄类开始奔跑 private void run() throws InterruptedException{ if (state == SPEED_UP) { System.out.println("--------------加速跑动---------------"); Thread.sleep(4000);//假设加速持续4秒 state = COMMON; System.out.println("------加速状态结束,变为正常状态------"); }else if (state == SPEED_DOWN) { System.out.println("--------------减速跑动---------------"); Thread.sleep(4000);//假设减速持续4秒 state = COMMON; System.out.println("------减速状态结束,变为正常状态------"); }else if (state == SWIM) { System.out.println("--------------不能跑动---------------"); Thread.sleep(2000);//假设眩晕持续2秒 state = COMMON; System.out.println("------眩晕状态结束,变为正常状态------"); }else { //正常跑动则不打印内容,否则会刷屏 } } }
下面我们写一个客户端类,去试图让英雄在各种状态下奔跑一下。
package com.state; public class Main { public static void main(String[] args) throws InterruptedException { Hero hero = new Hero(); hero.startRun(); hero.setState(Hero.SPEED_UP); Thread.sleep(5000); hero.setState(Hero.SPEED_DOWN); Thread.sleep(5000); hero.setState(Hero.SWIM); Thread.sleep(5000); hero.stopRun(); } }
可以看到,我们的英雄在跑动过程中随着状态的改变,会以不同的状态进行跑动。
在上面原始的例子当中,我们的英雄类当中有明显的if else结构,我们再来看看百度百科中状态模式所解决的问题的描述。
状态模式解决的问题:状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
不用说,状态模式是可以解决我们上面的if else结构的,我们采用状态模式,利用多态的特性可以消除掉if else结构。这样所带来的好处就是可以大大的增加程序的可维护性与扩展性。
下面我们就使用状态模式对上面的例子进行改善,首先第一步,就是我们需要定义一个状态接口,这个接口就只有一个方法,就是run。
package com.state; public interface RunState { void run(Hero hero); }
与状态模式类图不同的是,我们加入了一个参数Hero(Context),这样做的目的是为了具体的状态类当达到某一个条件的时候可以切换上下文的状态。下面列出四个具体的状态类,其实就是把if else拆掉放到这几个类的run方法中。
package com.state; public class CommonState implements RunState{ public void run(Hero hero) { //正常跑动则不打印内容,否则会刷屏 } }
package com.state; public class SpeedUpState implements RunState{ public void run(Hero hero) { System.out.println("--------------加速跑动---------------"); try { Thread.sleep(4000);//假设加速持续4秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------加速状态结束,变为正常状态------"); } }
package com.state; public class SpeedDownState implements RunState{ public void run(Hero hero) { System.out.println("--------------减速跑动---------------"); try { Thread.sleep(4000);//假设减速持续4秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------减速状态结束,变为正常状态------"); } }
package com.state; public class SwimState implements RunState{ public void run(Hero hero) { System.out.println("--------------不能跑动---------------"); try { Thread.sleep(2000);//假设眩晕持续2秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------眩晕状态结束,变为正常状态------"); } }
这下我们的英雄类也要相应的改动一下,最主要的改动就是那些if else可以删掉了,如下。
package com.state; //英雄类 public class Hero { public static final RunState COMMON = new CommonState();//正常状态 public static final RunState SPEED_UP = new SpeedUpState();//加速状态 public static final RunState SPEED_DOWN = new SpeedDownState();//减速状态 public static final RunState SWIM = new SwimState();//眩晕状态 private RunState state = COMMON;//默认是正常状态 private Thread runThread;//跑动线程 //设置状态 public void setState(RunState state) { this.state = state; } //停止跑动 public void stopRun() { if (isRunning()) runThread.interrupt(); System.out.println("--------------停止跑动---------------"); } //开始跑动 public void startRun() { if (isRunning()) { return; } final Hero hero = this; runThread = new Thread(new Runnable() { public void run() { while (!runThread.isInterrupted()) { state.run(hero); } } }); System.out.println("--------------开始跑动---------------"); runThread.start(); } private boolean isRunning(){ return runThread != null && !runThread.isInterrupted(); } }
可以看到,现在我们的英雄类优雅了许多,我们使用刚才同样的客户端运行即可得到同样的结果。
对比我们的原始例子,现在我们使用状态模式之后,有几个明显的优点:
一、我们去掉了if else结构,使得代码的可维护性更强,不易出错,这个优点挺明显,如果试图让你更改跑动的方法,是刚才的一堆if else好改,还是分成了若干个具体的状态类好改呢?答案是显而易见的。
二、使用多态代替了条件判断,这样我们代码的扩展性更强,比如要增加一些状态,假设有加速20%,加速10%,减速10%等等等(这并不是虚构,DOTA当中是真实存在这些状态的),会非常的容易。
三、状态是可以被共享的,这个在上面的例子当中有体现,看下Hero类当中的四个static final变量就知道了,因为状态类一般是没有自己的内部状态的,所有它只是一个具有行为的对象,因此是可以被共享的。
四、状态的转换更加简单安全,简单体现在状态的分割,因为我们把一堆if else分割成了若干个代码段分别放在几个具体的状态类当中,所以转换起来当然更简单,而且每次转换的时候我们只需要关注一个固定的状态到其他状态的转换。安全体现在类型安全,我们设置上下文的状态时,必须是状态接口的实现类,而不是原本的一个整数,这可以杜绝魔数以及不正确的状态码。
状态模式适用于某一个对象的行为取决于该对象的状态,并且该对象的状态会在运行时转换,又或者有很多的if else判断,而这些判断只是因为状态不同而不断的切换行为。
上面的适用场景是很多状态模式的介绍中都提到的,下面我们就来看下刚才DOTA中,英雄例子的类图。
可以看到,这个类图与状态模式的标准类图是几乎一模一样的,只是多了一条状态接口到上下文的依赖线,而这个是根据实际需要添加的,而且一般情况下都是需要的。
状态模式也有它的缺点,不过它的缺点和大多数模式相似,有两点。
1、会增加的类的数量。
2、使系统的复杂性增加。
尽管状态模式有着这样的缺点,但是往往我们牺牲复杂性去换取的高可维护性和扩展性是相当值得的,除非增加了复杂性以后,对于后者的提升会乎其微。
状态模式在项目当中也算是较经常会碰到的一个设计模式,但是通常情况下,我们还是在看到if else的情况下,对项目进行重构时使用,又或者你十分确定要做的项目会朝着状态模式发展,一般情况下,还是不建议在项目的初期使用。
好了,本次状态模式的分享就到此结束了,希望各位有所收获。