状态模式:实现交通红绿灯

1,453 阅读5分钟

状态模式的定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。 我们以逗号分割,把这句话分为两部分来看。第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。第二部分是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果。

状态模式实现红绿灯

红绿灯一般由红灯、绿灯、黄灯组成。红灯表示禁止通行,绿灯表示准许通行,黄灯表示警示。红绿灯控制机制均按照事先设定的配时方案运行。同一个红绿灯在不同的状态下,表现出来的行为是不一样的。

通常我们谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以红绿灯改变的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。同时我们还可以把状态的切换规则事先分布在状态类中,这样就有效地消除了原本存在的大量条件分支语句。

传统面向对象状态模式

现在用代码描述这个场景,首先将定义3个状态类, 分别是GreenLightState、YellowLightState、RedLightState。这三个类都有一个原型方法lightWasChange,代表在各自状态下,将发生的行为,代码如下:

const sleep = function(duration) {
  return new Promise(function(resolve, reject) {
      setTimeout(resolve, duration);
  })
}

class GreenLightState {
  constructor(light) {
    this.light = light;

    this.WAIT_TIME = 10000;
  }
  async lightWasChange() {
    console.log('绿灯亮了,准许通行!');
    await sleep(this.WAIT_TIME);
    this.light.setState(this.light.yellowLightState);
  }
}
class YellowLightState {
  constructor(light) {
    this.light = light;

    this.WAIT_TIME = 3000;
  }
  async lightWasChange() {
    console.log('黄灯亮了,警示灯要变红了!');
    await sleep(this.WAIT_TIME);
    this.light.setState(this.light.redLightState);
  }
}
class RedLightState {
  constructor(light) {
    this.light = light;

    this.WAIT_TIME = 5000;
  }
  async lightWasChange() {
    console.log('红灯亮了,禁止通行!');
    await sleep(this.WAIT_TIME);
    this.light.setState(this.light.greenLightState);
  }
}

接下来写Light类,我们在Light类的构造函数里为每个状态类都创建一个状态对象,这样一来我们可以很明显地看到红绿灯一共有多少种状态。红绿灯状态改变的行为将请求委托给当前持有的状态对象去执行,状态对象可以通过setState这个方法来切换light对象的状态。代码如下:

class Light {
  constructor() {
    this.redLightState = new RedLightState(this);
    this.greenLightState = new GreenLightState(this);
    this.yellowLightState = new YellowLightState(this);

    this.currentState = this.redLightState;
  }

  setState(newState) {
    this.currentState = newState;
    this.go();
  }

  go() {
    this.currentState.lightWasChange()
  }
}

现在可以进行一些测试:

let light = new Light();
light.go();

使用状态模式的好处很明显,它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码。另外,状态之间的切换都被分布在状态类内部,这使得我们无需编写过多的if、else条件分支语言来控制状态之间的转换。

JavaScript版本的状态机

状态模式是状态机的实现之一,但在JavaScript这种“无类”语言中,没有规定让状态对象一定要从类中创建而来。另外一点,JavaScript可以非常方便地使用委托技术,并不需要事先让一个对象持有另一个对象。下面的状态机选择了通过Function.prototype.call方法直接把请求委托给某个字面量对象来执行。

下面改写红绿灯的例子,来展示这种更加轻巧的做法:

const WAIT_TIME_GREEN = 10000;
const WAIT_TIME_YELLOW = 3000;
const WAIT_TIME_RED = 5000;

const sleep = function(duration) {
  return new Promise(function(resolve, reject) {
      setTimeout(resolve, duration);
  })
}

const FSM = {
  green: {
    async lightWasChange(){
      console.log('绿灯亮了,准许通行!');
      await sleep(WAIT_TIME_GREEN);
      this.setState(FSM.yellow);
    }
  },
  yellow: {
    async lightWasChange() {
      console.log('黄灯亮了,警示灯要变红了!');
      await sleep(WAIT_TIME_YELLOW);
      this.setState(FSM.red);
    },
  },
  red: {
    async lightWasChange() {
      console.log('红灯亮了,禁止通行!');
      await sleep(WAIT_TIME_RED);
      this.setState(FSM.green);
    }
  }
}

class Light {
  constructor() {
    this.currentState = FSM.red;
  }
  setState(newState) {
    this.currentState = newState;
    this.go();
  }
  go() {
    this.currentState.lightWasChange.call(this);
  }
}

let light = new Light();
light.go();

总结

状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。

策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。

它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。