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

状态模式实现红绿灯

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

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

传统面向对象状态模式

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

  1. const sleep = function(duration) {
  2. return new Promise(function(resolve, reject) {
  3. setTimeout(resolve, duration);
  4. })
  5. }
  6. class GreenLightState {
  7. constructor(light) {
  8. this.light = light;
  9. this.WAIT_TIME = 10000;
  10. }
  11. async lightWasChange() {
  12. console.log('绿灯亮了,准许通行!');
  13. await sleep(this.WAIT_TIME);
  14. this.light.setState(this.light.yellowLightState);
  15. }
  16. }
  17. class YellowLightState {
  18. constructor(light) {
  19. this.light = light;
  20. this.WAIT_TIME = 3000;
  21. }
  22. async lightWasChange() {
  23. console.log('黄灯亮了,警示灯要变红了!');
  24. await sleep(this.WAIT_TIME);
  25. this.light.setState(this.light.redLightState);
  26. }
  27. }
  28. class RedLightState {
  29. constructor(light) {
  30. this.light = light;
  31. this.WAIT_TIME = 5000;
  32. }
  33. async lightWasChange() {
  34. console.log('红灯亮了,禁止通行!');
  35. await sleep(this.WAIT_TIME);
  36. this.light.setState(this.light.greenLightState);
  37. }
  38. }

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

  1. class Light {
  2. constructor() {
  3. this.redLightState = new RedLightState(this);
  4. this.greenLightState = new GreenLightState(this);
  5. this.yellowLightState = new YellowLightState(this);
  6. this.currentState = this.redLightState;
  7. }
  8. setState(newState) {
  9. this.currentState = newState;
  10. this.go();
  11. }
  12. go() {
  13. this.currentState.lightWasChange()
  14. }
  15. }

现在可以进行一些测试:

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

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

JavaScript版本的状态机

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

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

  1. const WAIT_TIME_GREEN = 10000;
  2. const WAIT_TIME_YELLOW = 3000;
  3. const WAIT_TIME_RED = 5000;
  4. const sleep = function(duration) {
  5. return new Promise(function(resolve, reject) {
  6. setTimeout(resolve, duration);
  7. })
  8. }
  9. const FSM = {
  10. green: {
  11. async lightWasChange(){
  12. console.log('绿灯亮了,准许通行!');
  13. await sleep(WAIT_TIME_GREEN);
  14. this.setState(FSM.yellow);
  15. }
  16. },
  17. yellow: {
  18. async lightWasChange() {
  19. console.log('黄灯亮了,警示灯要变红了!');
  20. await sleep(WAIT_TIME_YELLOW);
  21. this.setState(FSM.red);
  22. },
  23. },
  24. red: {
  25. async lightWasChange() {
  26. console.log('红灯亮了,禁止通行!');
  27. await sleep(WAIT_TIME_RED);
  28. this.setState(FSM.green);
  29. }
  30. }
  31. }
  32. class Light {
  33. constructor() {
  34. this.currentState = FSM.red;
  35. }
  36. setState(newState) {
  37. this.currentState = newState;
  38. this.go();
  39. }
  40. go() {
  41. this.currentState.lightWasChange.call(this);
  42. }
  43. }
  44. let light = new Light();
  45. light.go();

总结

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

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

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