[JS设计模式]状态模式

169 阅读4分钟

前言

状态模式是一种很优秀的设计模式,可是在很多情况下它会增加开发的代码量。当然它的好处是可以极大的提高代码的语义化。因此掌握状态模式的核心思想,我们就等于在开发时拥有了在代码量与可读性两种情况间更多选择。

设计思路

状态模式的最常用的例子就是电灯。我们在控制一盏灯的开关时,最简单的模型就是一个开工按钮控制灯的状态即可。代码如下:

class Light{
  constructor(){
    this.state = 'off';
  }
  trigger(){
    this.state === 'off'?'on':'off';
  }
}

可是我们现在有一种更加高级的灯, 它的状态超过了2种。举个例子,一盏灯的状态有关闭,弱光,强光三种,而且他们的修改是按固定顺序修改的。这时我们的代码就要修改:

class Light{
  constructor(){
    this.state = 'off';
  }
  trigger(){
    if(this.state === 'off'){
      this.state = 'weak';
    }else if(this.state === 'weak'){
      this.state = 'strong';
    }else if(this.state === 'strong'){
      this.state = 'off';
    }
  }
}

观察上述代码,通过修改trigger的逻辑我们实现了三种状态的需求。可是这种代码设计肯定的是有问题的,它违反了开放-封闭原则。如果之后灯的状态增加,我们会不得不再继续扩展trigger的逻辑,这样显然不是长远之计。

状态模式

状态模式就是专门优化这类问题的 ,它的本质是通过把各种状态单独封成一个类。让电灯实例于当前状态的实例关联,以此实现代码的解耦。

// 关闭状态
class OffState{
  constructor(light){
     this.light = light;
  }
  trigger(){
    this.light.setState(this.light.weakLightState);
  }
}
// 弱光状态
class WeakLightState{
  constructor(light){
     this.light = light;
  }
  trigger(){
    this.light.setState(this.light.strongLightState);
  }
}
// 强光状态
class StrongLightState{
  constructor(light){
     this.light = light;
  }
  trigger(){
    this.light.setState(this.light.offState);
  }
}
class Light{
  constructor(){
    this.offState = new OffState(this);
    this.weakLightState = new WeakLightState(this);
    this.strongLightState = new StrongLightState(this);
    this.currentState = offState;
  }
  setState(state){
      this.currentState = state;
  }
}

const light = new Light();
// 当切换状态时
light.currentState.trigger();

这样就实现了一个简单的状态模式,简单来说就是:

  1. 为每个状态单独设计一个类
  2. 在每个状态类中定义trigger之后要切换的下一个状态
  3. 灯实例中的状态不再是字符,而是一个实例。
  4. 需要切换状态时,调用灯中状态实例的trigger。

这样如果之后想要增加状态,只新增一个状态类,在类中定义切换的下一个状态是什么,它是从什么状态切换过来的就可以。

class NewState{
  constructor(light){
     this.light = light;
  }
  trigger(){
    // 定义新状态下一个状态是什么
    this.light.setState(this.light.offState);
  }
}
// 定义新状态从哪里来
class StrongLightState{
  constructor(light){
     this.light = light;
  }
  trigger(){
    this.light.setState(this.light.newState);
  }
}
// 在灯中加入
class Light{
  constructor(){
    this.offState = new OffState(this);
    this.weakLightState = new WeakLightState(this);
    this.strongLightState = new StrongLightState(this);
    this.newState = new NewState(this);
    this.currentState = offState;
  }
  // ... 
}

细节优化

切换方法重写提示

现在我们已经实现了状态模式了。相信大家都能发现在这个框架下,状态类极度依赖规范的。以上方代码举例,我规定了每个状态类的切换方法是trigger。这样所以新增的状态类都必须要有这个方法,不然就会报错。可是JavaScript是一门动态语言,有时候很难避免有开发者在开发时漏了加入trigger。因此我们可以考虑在框架上加入一个抽象类——状态类。我们在抽象类中加入trigger,让每个状态继承于这个抽象类。这样即使真的出现有开发者忘了加入trigger之后,也能及时得到友好的提示。

// 抽象状态类
class State{
  trigger(){
    throw new Error('trigger方法必须被重写');
  }
}
// 状态类继承于它
class OffState extends State{
// ... 
}

开销优化

目前我们都是在灯的实例的初始化时创建了所有状态实例,并直接存在灯实例中。这种做法在状态数量不多时是更好的选择。可是假如状态数量很多时,这样灯实例的内存占用一定会增大。为了优化内存占用,我们可以改成在每次trigger的时候再实例状态。

class OffState extends State{
  //...
  trigger(){
     this.light.setState(new WeakLightState(this.light));
  }
}

这种改法可以有效地降低内存的占用,可是同时值得注意的是每次创建实例也是一笔运行开销,所以应该根据实际业务需求来决定采用哪种方式。

总结

现在相信大家已经对状态模式有了了解,它在处理多状态切换场景下可以为我们提的代码供更好维护性和降低耦合。但同时采用状态模式意味着需要为这段逻辑的代码量会增加。所以大家应该根据业务场景选择。

参考

《JavaScript设计模式与开发实践》—— 曾探