设计模式大冒险第五关:状态模式,if/else的“终结者”

这一篇文章是关于设计模式大冒险系列的第五篇文章,这一系列的每一篇文章我都但愿可以经过通俗易懂的语言描述或者平常生活中的小例子来帮助你们理解好每一种设计模式。javascript

今天这篇文章来跟你们一块儿学习一下状态模式。相信读完这篇文章以后,你会收获不少。在之后的开发中,若是遇到了相似的状况就知道如何更好地处理,可以少用ifelse语句,以及switch语句,写出更已读,扩展性更好,更易维护的程序。话很少说,咱们开始今天的文章吧。java

开发过程当中的一些场景

咱们在平时的开发过程当中,常常会遇到这样一种状况:就是须要咱们处理一个对象的不一样状态下的不一样行为。好比最多见的就是订单,订单有不少种状态,每种状态又对应着不一样的操做,有些操做是相同的,有些操做是不一样的。再好比一个音乐播放器程序,在播放器缓冲音乐,播放,暂停,快进,快退,终止等的状况下又对应着各类操做。有些操做在某些状况下是容许的,有些操做是不容许的。还有不少不一样的场景,这里就不一一列举了。git

那么面对上面说的这些状况咱们应该如何设计咱们的程序,才能让咱们开发出来的程序更好维护与扩展,也更方便别人阅读呢?先别着急,咱们一步一步来。遇到这种状况咱们应该首先把整个操做的状态图画出来,只有状态图画出来,咱们才能够清晰的知道这个过程当中会有哪些操做,都发生了哪些状态的改变。只要咱们作了这一步,而后按照状态图的逻辑去实现咱们的程序;先无论代码的质量如何,至少能够保证咱们的逻辑功能是知足了需求的。github

生活小例子,个人吹风机

让咱们从生活中的一个小例子入手吧。最近我家里新买了一个吹风机,这个吹风机有两个按钮。一个按钮控制吹风机的开关,另外一个按钮能够在吹风机打开的状况下切换吹风的模式。吹风机的模式有三种,分别是热风,冷热风交替,和冷风。而且吹风机打开时默认是热风模式segmentfault

若是让咱们来编写一个程序实现上面所说的吹风机的控制功能,咱们应该怎么实现呢?首先先别急着开始写代码,咱们须要把吹风机的状态图画出来。以下图所示:设计模式

吹风机的状态图

上面的状态图已经把吹风机的各类状态都表示出来了,其中圆圈表示了吹风机的状态,带箭头的线表示状态转换。从这个状态图咱们能够很直观的知道:吹风机从关闭状态到打开状态默认是热风模式,而后这三种模式能够按照顺序进行切换,而后在每一种模式下均可以直接关闭吹风机session

通常的实现方式

当咱们知道了整个吹风机的状态转换以后,咱们就能够开始写代码了。咱们先按照最直观的方式去实现咱们的代码。首先咱们知道吹风机有两个按钮,一个控制开关,一个控制吹风机的吹风模式。那么咱们的程序中须要有两个变量来分别表示开关状态吹风机当前所处的模式。这一部分的代码以下所示:app

function HairDryer() {
   // 定义内部状态 0:关机状态 1:开机状态
   this.isOn = 0;
   // 定义模式 0:热风 1:冷热风交替 2:冷风
   this.mode = 0;
}

接下来就要实现吹风机的开关按钮的功能了,这一部分比较简单;咱们只须要判断当前isOn变量,若是是打开状态就将isOn设置为关闭状态,若是是关闭状态就将isOn设置为打开状态。须要注意的一点就是在吹风机关闭的状况下须要将吹风机的模式重置为热风模式wordpress

// 切换吹风机的打开关闭状态
HairDryer.prototype.turnOnOrOff = function() {
   let { isOn, mode } = this;
   if (isOn === 0) {
      // 打开吹风机
      isOn = 1;
      console.log('吹风机的状态变为:[打开状态],模式是:[热风模式]');
   } else {
      // 关闭吹风机
      isOn = 0;
      // 重置吹风机的模式
        mode = 0;
      console.log('吹风机的状态变为:[关闭状态]');
   }
   this.isOn = isOn;
   this.mode = mode;
};

在接下来就是实现吹风机的模式切换的功能了,代码以下所示:函数

// 切换吹风机的模式
HairDryer.prototype.switchMode = function() {
   const { isOn } = this;
   let { mode } = this;
   // 切换模式的前提是:吹风机是开启状态
   if (isOn === 1) {
      // 须要知道当前模式
      if (mode === 0) {
         // 若是当前是热风模式,切换以后就是冷热风交替模式
         mode = 1;
         console.log('吹风机的模式改变为:[冷热风交替模式]');
      } else if (mode === 1) {
         // 若是当前是冷热风交替模式,切换以后就是冷风模式
         mode = 2;
         console.log('吹风机的模式改变为:[冷风模式]');
      } else {
         // 若是当前是冷风模式,切换以后就是热风模式
         mode = 0;
         console.log('吹风机的模式改变为:[热风模式]');
      }
   } else {
      console.log('吹风机在关闭的状态下没法改变模式');
   }
   this.mode = mode;
};

这一部分的代码也不算难,可是有一些细节须要注意。首先咱们切换模式须要吹风机是打开的状态,而后当吹风机是关闭的状态的时候,咱们不可以切换模式。到这里为止,咱们已经把吹风机的控制功能都实现了。接下来就要写一些代码来验证一下咱们上面的程序是否正确,测试的代码以下所示:

const hairDryer = new HairDryer();
// 打开吹风机,切换吹风机模式
hairDryer.turnOnOrOff();
hairDryer.switchMode();
hairDryer.switchMode();
hairDryer.switchMode();
// 关闭吹风机,尝试切换模式
hairDryer.turnOnOrOff();
hairDryer.switchMode();
// 打开关闭吹风机
hairDryer.turnOnOrOff();
hairDryer.turnOnOrOff();

输出的结果以下所示:

吹风机的状态变为:[打开状态],模式是:[热风模式]
吹风机的模式改变为:[冷热风交替模式]
吹风机的模式改变为:[冷风模式]
吹风机的模式改变为:[热风模式]
吹风机的状态变为:[关闭状态]
吹风机在关闭的状态下没法改变模式
吹风机的状态变为:[打开状态],模式是:[热风模式]
吹风机的状态变为:[关闭状态]

从上面测试的结果咱们能够知道,上面程序编写的逻辑是没有问题的,实现了咱们想要的预期的功能。若是想看上面代码的完整版本能够点击这里浏览。

可是你能从上面的代码中看出什么问题吗?做为一个优秀的工程师,你确定会发现上面的代码使用了太多的if/else判断,而后切换吹风机模式的代码都耦合在一块儿。这样会致使一些问题,首先上面代码的可读性不是很好,若是没有注释的话,想要知道吹风机模式的切换逻辑仍是有点费力的。另外一方面,上面代码的可扩展性也不是很好,若是咱们想新增长一种模式的话,就须要修改if/else里面的判断,很容易出错。那么做为一个优秀的工程师,咱们该如何重构上面的程序呢?

状态模式的介绍,以及使用状态模式重构以前的程序

接下来咱们就要进入状态模式的学习过程了,首先咱们先不用管什么是状态模式。咱们先来再次看一下上面关于吹风机的状态图,咱们能够看到吹风机在整个过程当中有四种状态,分别是:关闭状态热风模式状态冷热风交替模式状态冷风模式状态。而后这四种模式分别都有两个操做,分别是切换模式切换吹风机的打开和关闭状态。(注:对于关闭状态,虽然没法切换模式,可是在这里咱们也认为这种状态有这个操做,只是操做不会起做用。)

那么咱们是否是能够换一种思路去解决这个问题,咱们能够把具体的操做封装进每个状态里面,而后由对应的状态去处理对应的操做。咱们只须要控制好状态之间的切换就能够了。这样作可让咱们把相应的操做委托给相应的状态去作,不须要再写那么多的if/else去判断状态,这样作还可让咱们把变化封装进对应的状态中去。若是须要添加新的状态,咱们对原来的代码的改动也会很小

状态模式的简单介绍

那么到这里咱们来介绍一下状态模式吧,状态模式指的是:可以在对象的内部状态改变的时候改变对象的行为状态模式经常用来对一个对象在不一样状态下一样操做时产生的不一样行为进行封装,从而达到可让对象在运行时改变其行为的能力。就像咱们上面说的吹风机,在热风模式下,按下模式切换按钮能够切换到冷热风交替模式;可是若是当前状态是冷热风交替模式,那么按下模式切换按钮,就切换到了冷风模式了。更详细的解释能够参考State pattern

咱们再来看一下状态模式的UML图,以下所示:

状态模式的UML图

能够看到,对于状态模式来讲,有一个Context(上下文),一个抽象的State类,这个抽象类定义好了每个具体的类须要实现的方法。对于每个具体的类来讲,它实现了抽象类State定义好的方法,而后Context在须要进行操做的时候,只须要请求对应具体状态类实例的对应方法就能够了

使用状态模式来重构以前的程序

接下来咱们来用状态模式来重构咱们的程序,首先是Context,对应的代码以下所示:

// 状态模式
// 吹风机
class HairDryer {
   // 吹风机的状态
   state;
   // 关机状态
   offState;
   // 开机热风状态
   hotAirState;
   // 开机冷热风交替状态
   alternateHotAndColdAirState;
   // 开机冷风状态
   coldAirState;

   // 构造函数
   constructor(state) {
      this.offState = new OffState(this);
      this.hotAirState = new HotAirState(this);
      this.alternateHotAndColdAirState = new AlternateHotAndColdAirState(
         this
      );
      this.coldAirState = new ColdAirState(this);
      if (state) {
         this.state = state;
      } else {
         // 默认是关机状态
         this.state = this.offState;
      }
   }

   // 设置吹风机的状态
   setState(state) {
      this.state = state;
   }

   // 开关机按钮
   turnOnOrOff() {
      this.state.turnOnOrOff();
   }
   // 切换模式按钮
   switchMode() {
      this.state.switchMode();
   }

   // 获取吹风机的关机状态
   getOffState() {
      return this.offState;
   }
   // 获取吹风机的开机热风状态
   getHotAirState() {
      return this.hotAirState;
   }
   // 获取吹风机的开机冷热风交替状态
   getAlternateHotAndColdAirState() {
      return this.alternateHotAndColdAirState;
   }
   // 获取吹风机的开机冷风状态
   getColdAirState() {
      return this.coldAirState;
   }
}

我来解释一下上面的代码,首先咱们使用HairDryer来表示Context,而后HairDryer类的实例属性有state,这属性就是表示了吹风机当前所处的状态。其他的四个属性分别表示吹风机对应的四个状态实例。

吹风机有setState能够设置吹风机的状态,而后getOffStategetHotAirStategetAlternateHotAndColdAirStategetColdAirState分别用来获取吹风机的对应状态实例。你可能会说为何要在HairDryer类里面获取相应的状态实例呢?别着急,下面会解释为何。

而后turnOnOrOff方法表示打开或者关闭吹风机,switchMode用来表示切换吹风机的模式。还有constructor,咱们默认若是没有传递状态实例的话,默认是热风模式状态。

而后是咱们的抽象类State,由于咱们的实现使用的语言是JavaScriptJavaScript暂时还不支持抽象类,因此用通常的类来代替。这个对咱们实现状态模式没有太大的影响。具体的代码以下:

// 抽象的状态
class State {
   // 开关机按钮
   turnOnOrOff() {
      console.log('---按下吹风机 [开关机] 按钮---');
   }
   // 切换模式按钮
   switchMode() {
      console.log('---按下吹风机 [模式切换] 按钮---');
   }
}

State类主要是用来定义好具体的状态类应该实现的方法,对于咱们这个吹风机的例子来讲就是turnOnOrOffswitchMode。它们分别对应,按下吹风机开关机按钮的处理和按下吹风机的模式切换按钮的处理。

接下来就是具体的状态类的实现了,代码以下所示:

// 吹风机的关机状态
class OffState extends State {
   // 吹风机对象的引用
   hairDryer;
   constructor(hairDryer) {
      super();
      this.hairDryer = hairDryer;
   }
   // 开关机按钮
   turnOnOrOff() {
      super.turnOnOrOff();
      this.hairDryer.setState(this.hairDryer.getHotAirState());
      console.log('状态切换: 关闭状态 => 开机热风状态');
   }
   // 切换模式按钮
   switchMode() {
      console.log('===吹风机在关闭的状态下没法切换模式===');
   }
}

// 吹风机的开机热风状态
class HotAirState extends State {
   // 吹风机对象的引用
   hairDryer;
   constructor(hairDryer) {
      super();
      this.hairDryer = hairDryer;
   }
   // 开关机按钮
   turnOnOrOff() {
      super.turnOnOrOff();
      this.hairDryer.setState(this.hairDryer.getOffState());
      console.log('状态切换: 开机热风状态 => 关闭状态');
   }
   // 切换模式按钮
   switchMode() {
      super.switchMode();
      this.hairDryer.setState(
         this.hairDryer.getAlternateHotAndColdAirState()
      );
      console.log('状态切换: 开机热风状态 => 开机冷热风交替状态');
   }
}

// 吹风机的开机冷热风交替状态
class AlternateHotAndColdAirState extends State {
   // 吹风机对象的引用
   hairDryer;
   constructor(hairDryer) {
      super();
      this.hairDryer = hairDryer;
   }
   // 开关机按钮
   turnOnOrOff() {
      super.turnOnOrOff();
      this.hairDryer.setState(this.hairDryer.getOffState());
      console.log('状态切换: 开机冷热风交替状态 => 关闭状态');
   }
   // 切换模式按钮
   switchMode() {
      super.switchMode();
      this.hairDryer.setState(this.hairDryer.getColdAirState());
      console.log('状态切换: 开机冷热风交替状态 => 开机冷风状态');
   }
}

// 吹风机的开机冷风状态
class ColdAirState extends State {
   // 吹风机对象的引用
   hairDryer;
   constructor(hairDryer) {
      super();
      this.hairDryer = hairDryer;
   }
   // 开关机按钮
   turnOnOrOff() {
      super.turnOnOrOff();
      this.hairDryer.setState(this.hairDryer.getOffState());
      console.log('状态切换: 开机冷风状态 => 关闭状态');
   }
   // 切换模式按钮
   switchMode() {
      super.switchMode();
      this.hairDryer.setState(this.hairDryer.getHotAirState());
      console.log('状态切换: 开机冷风状态 => 开机热风状态');
   }
}

由上面的代码咱们能够看到,对于每个具体的类来讲,都有一个属性hairDryer,这个属性用来保存吹风机实例的索引。而后就是对应turnOnOrOffswitchMode方法的实现。咱们能够看到在具体的类中咱们设置hairDryer的状态是经过hairDryer实例的setState方法,而后获取状态是经过hairDryer对应的获取状态的方法。好比:this.hairDryer.getHotAirState()就是获取吹风机的热风模式状态。

在这里咱们能够说一下为何要在HairDryer类里面获取相应的状态实例:由于这样不一样的状态类之间至关于解耦了,它们不须要在各自的类中依赖对应的状态,直接从hairDryer实例上获取对应的状态实例就能够了。减小了类之间的依赖,使咱们代码的可维护性变的更好了

接下来就是须要测试一下咱们上面经过状态模式重构后的代码有没有实现咱们想要的功能,测试的代码以下:

const hairDryer = new HairDryer();
// 打开吹风机
hairDryer.turnOnOrOff();
// 切换模式
hairDryer.switchMode();
// 切换模式
hairDryer.switchMode();
// 切换模式
hairDryer.switchMode();
// 关闭吹风机
hairDryer.turnOnOrOff();
// 吹风机在关闭的状态下没法切换模式
hairDryer.switchMode();

输出的结果以下所示:

---按下吹风机 [开关机] 按钮---
状态切换: 关闭状态 => 开机热风状态
---按下吹风机 [模式切换] 按钮---
状态切换: 开机热风状态 => 开机冷热风交替状态
---按下吹风机 [模式切换] 按钮---
状态切换: 开机冷热风交替状态 => 开机冷风状态
---按下吹风机 [模式切换] 按钮---
状态切换: 开机冷风状态 => 开机热风状态
---按下吹风机 [开关机] 按钮---
状态切换: 开机热风状态 => 关闭状态
===吹风机在关闭的状态下没法切换模式===

根据上面的测试结果能够知道,咱们重构以后的代码也完美地实现了咱们想要的功能。使用状态模式重构后的完整版本能够点击这里浏览。那么接下来咱们就来分析一下,使用状态模式与第一种不使用状态模式相比有哪些优点和劣势。

使用状态模式的优点有如下几个方面:

  • 将应用的代码解耦,利于阅读和维护。咱们能够看到,在第一种方案中,咱们使用了大量的if/else来进行逻辑的判断,将各类状态和逻辑放在一块儿进行处理。在咱们应用相关对象的状态比较少的状况下可能不会有太大的问题,可是一旦对象的状态变得多了起来,这种耦合比较深的代码维护起来就很困难,很折磨人。
  • 将变化封装进具体的状态对象中,至关于将变化局部化,而且进行了封装。利于之后的维护与拓展。使用状态模式以后,咱们把相关的操做都封装进对应的状态中,若是想修改或者添加新的状态,也是很方便的。对代码的修改也比较少,扩展性比较好。
  • 经过组合和委托,让对象在运行的时候能够经过改变状态来改变本身的行为。咱们只须要将对象的状态图画出来,专一于对象的状态改变,以及每一个状态有哪些行为。这让咱们的开发变得简单一些,也不容易出错,可以保证咱们写出来的代码质量是不错的。

使用状态模式的劣势:

  • 固然使用状态模式也有一点劣势,那就是增长了代码中类的数量,也就是增长了代码量。可是在绝大多数状况下来讲,这个算不上什么太大的问题。除非你开发的应用对代码量有着比较严格的要求。

状态模式的总结

经过上面对状态模式的讲解,以及吹风机小例子的实践,相信你们对状态模式都有了很深刻的理解。在平时的开发工做中,若是一个对象有不少种状态,而且这个对象在不一样状态下的行为也不同,那么咱们就可使用状态模式来解决这个问题。使用状态模式可让咱们的代码条理清楚,容易阅读;也方便维护和扩展

为了验证你的确已经掌握了状态模式,这里给你们出个小题目。仍是以上面的吹风机为例子,若是如今吹风机新增长了一个按钮,用来切换风速强度的大小。默认风速的强度是弱风,按下按钮变为强风。如今你能修改上面的代码,而后实现这个功能吗,赶快动手试试吧~

文章到这里就结束了,若是你们有什么问题和疑问欢迎你们在文章下面留言,或者在dreamapplehappy/blog提出来。也欢迎你们关注个人公众号关山不难越,获取更多关于设计模式讲解的内容。

下面是这一系列的其它的文章,也欢迎你们阅读,但愿你们都可以掌握好这些设计模式的使用场景和解决的方法。若是这篇文章对你有所帮助,那就点个赞,分享一下吧~

参考连接:

相关文章
相关标签/搜索