状态模式

在软件开发过程中,应用程序中的部分对象可能会根据不同的情况做出不同的行为,我们把这种对象称为有状态的对象,而把影响对象行为的一个或多个动态变化的属性称为状态。当有状态的对象与外部事件产生互动时,其内部状态就会发生改变,从而使其行为也发生改变。如人都有高兴和伤心的时候,不同的情绪有不同的行为,当然外界也会影响其情绪变化。
对这种有状态的对象编程,传统的解决方案是:将这些所有可能发生的情况全都考虑到,然后使用 if-else 或 switch-case 语句来做状态判断,再进行不同情况的处理。但是显然这种做法对复杂的状态判断存在天然弊端,条件判断语句会过于臃肿,可读性差,且不具备扩展性,维护难度也大。且增加新的状态时要添加新的 if-else 语句,这违背了“开闭原则”,不利于程序的扩展。
以上问题如果采用“状态模式”就能很好地得到解决。状态模式的解决思想是:当控制一个对象状态转换的条件表达式过于复杂时,把相关“判断逻辑”提取出来,用各个不同的类进行表示,系统处于哪种情况,直接使用相应的状态类对象进行处理,这样能把原来复杂的逻辑判断简单化,消除了 if-else、switch-case 等冗余语句,代码更有层次性,并且具备良好的扩展力。


状态模式的定义与特点

状态(State)模式的定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
状态模式是一种对象行为型模式,其主要优点如下。

  1. 结构清晰,状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
  2. 将状态转换显示化,减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
  3. 状态类职责明确,有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。

状态模式的主要缺点如下。

  1. 状态模式的使用必然会增加系统的类与对象的个数。
  2. 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
  3. 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。


状态模式的实现

比如超级玛丽,就可能同时有好几个状态比如 跳跃,移动,射击,蹲下 等,如果对这些动作一个个进行处理判断,需要多个if-else或者switch不仅丑陋不说,而且在遇到有组合动作的时候,实现就会变的更为复杂,这里可以使用状态模式来实现。

状态模式的思路是:首先创建一个状态对象或者数组,内部保存状态变量,然后内部封装好每种动作对应的状态,然后状态对象返回一个接口对象,它可以对内部的状态修改或者调用。

const SuperMarry = (function() {    
  let _currentState = [],        // 状态数组
      states = {
        jump() {console.log('跳跃!')},
        move() {console.log('移动!')},
        shoot() {console.log('射击!')},
        squat() {console.log('蹲下!')}
      }
  
  const Action = {
    changeState(arr) {  // 更改当前动作
      _currentState = arr
      return this
    },
    goes() {
      console.log('触发动作')
      _currentState.forEach(T => states[T] && states[T]())
      return this
    }
  }
  
  return {
    change: Action.changeState,
    go: Action.goes
  }
})()

SuperMarry
    .change(['jump', 'shoot'])
    .go()                    // 触发动作  跳跃!  射击!
    .go()                    // 触发动作  跳跃!  射击!
    .change(['squat'])
    .go()                    // 触发动作  蹲下!

这里可以使用ES6的class优化一下:

class SuperMarry {
  constructor() {
    this._currentState = []
    this.states = {
      jump() {console.log('跳跃!')},
      move() {console.log('移动!')},
      shoot() {console.log('射击!')},
      squat() {console.log('蹲下!')}
    }
  }
  
  change(arr) {  // 更改当前动作
    this._currentState = arr
    return this
  }
  
  go() {
    console.log('触发动作')
    this._currentState.forEach(T => this.states[T] && this.states[T]())
    return this
  }
}

new SuperMarry()
    .change(['jump', 'shoot'])
    .go()                    // 触发动作  跳跃!  射击!
    .go()                    // 触发动作  跳跃!  射击!
    .change(['squat'])
    .go()                    // 触发动作  蹲下!


总结

状态模式的使用场景也特别明确,有如下两点:

  1. 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。
  2. 一个操作中含有大量的分支语句,而且这些分支语句依赖于该对象的状态。状态通常为一个或多个枚举常量的表示。

简而言之,当遇到很多同级if-else或者switch的时候,可以使用状态模式来进行简化。


链接: https://www.fly63.com/course/27_1275