『面试的底气』—— 设计模式之状态模式

2,785 阅读5分钟

定义

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

我们以逗号分割,把这句话分为两部分来看。

第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。

第二部分是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果。

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。

为什么要用状态模式

例如有一个场景,有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。

先用代码来实现一下。

class Light {
  constructor() {
    this.state = 'off';
    this.button = null;
  }
  init() {
    const button = document.createElement('button');
    const self = this;
    button.innerHTML = '开关';
    this.button = document.body.appendChild(button);
    this.button.onclick = function () {
      self.buttonWasPressed();
    }
  }
  buttonWasPressed () {
    if (this.state === 'off') {
      console.log('开灯');
      this.state = 'on';
    } else if (this.state === 'on') {
      console.log('关灯');
      this.state = 'off';
    }
  }
}
const light = new Light();
light.init();

现在出现了另一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。那么此时必须改造buttonWasPressed方法才能实现这种电灯。

buttonWasPressed() {
  if (this.state === 'off') {
    console.log('弱光');
    this.state = 'weakLight';
  } else if (this.state === 'weakLight') {
    console.log('强光');
    this.state = 'strongLight';
  } else if (this.state === 'strongLight') {
    console.log('关灯');
    this.state = 'off';
  }
}

虽然实现了需求,但是违背了开放--封闭原则,每次新增或者修改light的状态,都需要改动buttonWasPressed方法中的代码,这使得buttonWasPressed成为了一个非常不稳定的方法。

所有跟灯的状态有关的行为,都被封装在buttonWasPressed方法里,如果以后这个电灯又增加了强强光、超强光和终极强光,那我们将无法预计这个方法将膨胀到什么地步。当然为了简化示例,此处在状态发生改变的时候,只是简单地打印一条 log 和改变 button 的innerHTML。在实际开发中,要处理的事情可能比这多得多,也就是说,buttonWasPressed方法要比现在庞大得多。

同时灯的状态的切换非常不明显,仅仅表现为对state属性赋值,比如this.state = 'weakLight'。 在实际开发中,这样的操作很容易被程序员不小心漏掉。我们也没有办法一目了然地明白电灯一共有多少种状态,除非耐心地读完buttonWasPressed方法里的所有代码。当状态的种类多起来的时候,某一次切换的过程就好像被埋藏在一个巨大方法,非常难寻找。

而且状态之间的切换关系,不过是往buttonWasPressed方法里堆砌if、else语句,增加或者修 改一个状态可能需要改变若干个操作,这使buttonWasPressed更加难以阅读和维护。

此时就可以用状态模式重构以上代码,来改变buttonWasPressed方法的弊端。

状态模式如何使用

状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以 button 被按下的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。

那么先来封装电灯的状态。

class OffLightState{
  constructor(light){
    this.light = light;
  }
  buttonWasPressed(){
    console.log( '弱光' ); // offLightState 对应的行为
    this.light.setState( this.light.weakLightState ); // 切换状态到 weakLightState
  }
}

class WeakLightState{
  constructor(light){
    this.light = light;
  }
  buttonWasPressed(){
    console.log( '强光' ); // weakLightState 对应的行为
    this.light.setState( this.light.strongLightState ); // 切换状态到 strongLightState
  }
}

class StrongLightState{
  constructor(light){
    this.light = light;
  }
  buttonWasPressed(){
    console.log( '关灯' ); // strongLightState 对应的行为
    this.light.setState( this.light.offLightState ); // 切换状态到 offLightState
  }
}

封装完电灯的状态,那么要改造一下 Light类。

class Light {
  constructor() {
    this.offLightState = new OffLightState(this);
    this.weakLightState = new WeakLightState(this);
    this.strongLightState = new StrongLightState(this);
    this.button = null;
  }
  init() {
    const button = document.createElement('button');
    const self = this;
    this.button = document.body.appendChild(button);
    this.button.innerHTML = '开关';
    this.currState = this.offLightState; // 设置当前状态
    this.button.onclick = function () {
      self.currState.buttonWasPressed();
    }
  }
  setState(newState) {
    this.currState = newState;
  }
}

const light = new Light();
light.init();

可以发现,现在Light类的构造函数中不再使用一个字符串来记录当前的状态,而是使用更加立体化的状态对象。在Light类的构造函数里为每个状态类都创建一个状态对象,这样一来我们可以很明显地看到电灯一共有多少种状态。

init方法中,在 button 按钮被按下的事件里,Context 也不再直接进行任何实质性的操作,而是通过 self.currState.buttonWasPressed()将请求委托给当前持有的状态对象去执行。

最后还要提供一个setState方法,状态对象可以通过这个方法来切换 light 对象的状态。

以上就实现了一个非常简单的状态模式。

状态模式的优点

从状态模式的实现过程中,可以很清晰的看到一些状态模式的优点。

  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。

  • 避免切换状态的方法无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了切换状态的方法中原本过 多的条件分支。

  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。

  • 切换状态的方法中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。