乐趣区

关于javascript:设计模式大冒险第五关状态模式ifelse的终结者

这一篇文章是对于设计模式大冒险系列的第五篇文章,这一系列的每一篇文章我都心愿可能通过通俗易懂的语言形容或者日常生活中的小例子来帮忙大家了解好每一种设计模式。

明天这篇文章来跟大家一起学习一下状态模式。置信读完这篇文章之后,你会播种很多。在当前的开发中,如果遇到了相似的状况就晓得如何更好地解决,可能少用 ifelse语句,以及 switch 语句,写出更已读,扩展性更好,更易保护的程序。话不多说,咱们开始明天的文章吧。

开发过程中的一些场景

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

那么面对下面说的这些状况咱们应该 如何设计咱们的程序,能力让咱们开发进去的程序更好保护与扩大,也更不便他人浏览呢 ?先别着急,咱们一步一步来。遇到这种状况咱们应该首先把整个操作的状态图画进去, 只有状态图画进去,咱们才能够清晰的晓得这个过程中会有哪些操作,都产生了哪些状态的扭转。只有咱们做了这一步,而后依照状态图的逻辑去实现咱们的程序;先不论代码的品质如何,至多能够保障咱们的逻辑性能是满足了需要的。

生存小例子,我的吹风机

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

如果让咱们来编写一个程序实现下面所说的吹风机的管制性能,咱们应该怎么实现呢?首先先别急着开始写代码,咱们须要把吹风机的状态图画进去。如下图所示:

下面的状态图曾经把吹风机的各种状态都示意进去了,其中圆圈示意了吹风机的状态,带箭头的线示意状态转换 。从这个状态图咱们能够很直观的晓得: 吹风机从敞开状态到关上状态默认是热风模式,而后这三种模式能够依照程序进行切换,而后在每一种模式下都能够间接敞开吹风机

个别的实现形式

当咱们晓得了整个吹风机的状态转换之后,咱们就能够开始写代码了。咱们先依照最直观的形式去实现咱们的代码。首先咱们晓得吹风机有两个按钮,一个管制开关,一个管制吹风机的吹风模式。那么咱们的程序中须要有两个变量来别离示意 开关状态 吹风机以后所处的模式。这一部分的代码如下所示:

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

接下来就要实现吹风机的开关按钮的性能了,这一部分比较简单;咱们只须要判断以后 isOn 变量,如果是关上状态就将 isOn 设置为敞开状态,如果是敞开状态就将 isOn 设置为关上状态。须要留神的一点就是在吹风机敞开的状况下须要将吹风机的模式重置为热风模式

// 切换吹风机的关上敞开状态
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 图,如下所示:

能够看到,对于状态模式来说,有一个 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 提出来。也欢送大家关注我的公众号关山不难越,获取更多对于设计模式解说的内容。

上面是这一系列的其它的文章,也欢送大家浏览,心愿大家都可能把握好这些设计模式的应用场景和解决的办法。如果这篇文章对你有所帮忙,那就点个赞,分享一下吧~

  • 设计模式大冒险第四关:单例模式,如何成为你的“惟一”
  • 设计模式大冒险第三关:工厂模式,封装和解耦你的代码
  • 设计模式大冒险第二关:装璜者模式,煎饼果子的主场
  • 设计模式大冒险第一关:观察者模式

参考链接:

  • State pattern,文中状态模式的 UML 图片也来自这里。
  • Take Control of your App with the JavaScript State Pattern
  • The State Design Pattern vs State Machine
退出移动版