状态模式的定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。 我们以逗号分割,把这句话分为两部分来看。第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。第二部分是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果。
状态模式实现红绿灯
红绿灯一般由红灯、绿灯、黄灯组成。红灯表示禁止通行,绿灯表示准许通行,黄灯表示警示。红绿灯控制机制均按照事先设定的配时方案运行。同一个红绿灯在不同的状态下,表现出来的行为是不一样的。
通常我们谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以红绿灯改变的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。同时我们还可以把状态的切换规则事先分布在状态类中,这样就有效地消除了原本存在的大量条件分支语句。
传统面向对象状态模式
现在用代码描述这个场景,首先将定义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();
总结
状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。
策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。
它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。