JavaScript设计模式十四(状态模式)
定义:
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为的改变
首先看一个场景
开灯关灯
有一个电灯,电灯上只要一个开关。当电灯开着的时候,按下开关,电灯会切换到关闭状态;再次按下开关,电灯会被打开。同一个开关按钮,在不同的状态下,变现的行为是不一样的。我们看一下如何实现:
var Light = function() {
this.state = 'off';
this.button = null;
};
Light.prototype.init = function() {
var button = document.createElement('button');
var self = this;
button.innerHTML = '开关';
this.button = document.body.appendChild(button);
this.button.onclick= function() {
self.buttonWasPressed();
}
}
Light.prototype.buttonWasPressed = function() {
if (this.state === 'off') {
console.log('开灯');
this.state = 'on';
} else if (this.state === 'on') {
console.log('关灯');
this.state = 'off';
}
}
var light = new Light();
light.init();
其实这个就是一个状态机,我们需要交替切换button的class,通常我们会用一个变量state来记录状态,然后在事件发生后,再根据这个状态来决定下一步的行为。
提问?上面那个代码有什么缺点?
我们知道现在的灯不仅仅是简单的打开关闭,有些还有强光,弱光,不同颜色的光,那么当这种需求出来的时候,我们就需要去修改buttonWasPressed的代码,上面的代码的缺点:
- buttonWasPressed违反了开放-封闭原则,每次新增状态都要去修改
- buttonWasPressed函数将和状态有关的行为都写到了一起,后面这个函数可能会膨胀很大
- 状态的切换非常不明显,仅仅是对state进行赋值
- 状态之间的切换关系,不过是往buttonWasPressed方法里面堆砌if、else语句,增加或者修改一个状态可能需要修改若干的操作,这使buttonWasPressed很难阅读
接下来我们用状态模式来改写程序:
一般来说我们都是封装对象的行为,而不是对象的状态。但是状态模式中正好是相反的,我们通常把每种状态都封装成一个单独的类,当状态改变的时候,我们只需要把请求委托给当前的对象就可以了。
状态切换:
代码:
var OffLightState = function(light) {
this.light = light;
}
OffLightState.prototype.buttonWasPressed = function() {
console.log('弱光');
this.light.setState(this.light.weakLightState);
}
var WeakLightState = function(light) {
this.light = light;
}
WeakLightState.prototype.buttonWasPressed = function() {
console.log('强光');
this.light.setState(this.light.strongLightState);
}
var StrongLightState = function(light) {
this.light = light;
}
StrongLightState.prototype.buttonWasPressed = function() {
console.log('关灯');
this.light.setState(this.light.offLightState);
}
// 改写Light类
var Light = function() {
this.offLightState = new OffLightState(this);
this.weakLightState = new WeakLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
}
Light.prototype.init = function() {
var button = document.createElement('button');
var self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = '开关';
this.currState = this.offLightState;
this.button.onclick = function() {
self.currState.buttonWasPressed();
}
}
Light.prototype.setState = function(newState) {
this.currState = newState;
}
我们看到这份代码把每种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类中,便于阅读和管理代码。
当我们需要增加一个新的状态是很方便,只需要增加一个类,然后修改init方法就可以了
我们看到上面的那些状态类中必须实现一个buttonWasPressed的方法,但是总是会有些人忘记,Java中有抽象类,JavaScript可以设置一个父类,在没有实现的时候抛出异常。
var State = function() {};
State.prototype.buttonWasPressed = function() {
throw new Error('父类的buttonWasPressed必须被重写');
}
var SuperStrongLightState = function(light) {
this.light = light;
}
SuperStrongLightState.prototype = new State();
SuperStrongLightState.prototype.buttonWasPressed = function() {
console.log('关灯');
this.light.setState(this.light.offLightState);
}
状态模式的优缺点
优点:
- 状态模式定义了状态和行为之间的关系,并将它们封装在一个类里面,通过增加新的状态类,很容易增加新的状态和转换
- 避免Context的无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本的if、else分支逻辑
- 用对象代理字符串来记录当前的状态,状态的切换一目了然
- Context中的请求动作和状态中封装的行为可以非常容易的独立变化而互不影响
缺点就是要写很多状态类,想想20个、50个,哈哈哈
JavaScript中的状态机
上面的例子是模拟传统的面向对象来实现的,我们看如何用JavaScript来实现状态机
我们知道在JavaScript中,没有规定让状态对象一定要从类中创建来,此外JavaScript可以很方便的时候委托技术,例如call方法和apply方法
var Light = function() {
this.currState = FSM.off;
this.button = null;
}
Light.prototype.init = function() {
var button = document.createElement('button');
var self = this;
button.innerHTML = '已关灯';
this.button = docuemnt.body.appendChild(button);
this.button.onclick = function() {
self.currState.buttonWasPressed.call(self);
}
}
var FSM = {
off: {
buttonWasPressed: function() {
console.log('关灯');
this.currState = FSM.on;
}
},
on: {
buttonWasPressed: function() {
console.log('开灯');
this.currState = FSM.off;
}
}
}
var light = new Light();
light.init();