JavaScript设计模式———状态模式
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
状态模式: 将事物内部的每个状态分别封装成类, 内部状态改变会产生不同行为。
通常我们谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。
例子:
某某牌电灯, 按一下按钮打开弱光, 按两下按钮打开强光, 按三下按钮关闭灯光。
普通版本:
var Light = function() {
this.state = 'off'; // 给电灯设置初始状态 off
this.button = null; // 电灯开关按钮
};
Light.prototype.init = function() {
var button = document.createElement('button'),
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();
以上版本缺点:
- 很明显 buttonWasPressed 方法是违反开放-封闭原则的,每次新增或者修改 light 的状态,都需要改动 buttonWasPressed 方法中的代码,这使得 buttonWasPressed 成为了一个非常不稳定的方法
- 所有跟状态有关的行为,都被封装在 buttonWasPressed 方法里;如果以后增加状态和其他行为时,我们将无法预计这个方法将膨胀到什么地步,buttonWasPressed 将会很庞大且无法复杂。
- 状态的切换非常不明显,仅仅表现为对 state 变量赋值,比如 this.state = ‘weakLight’。在实际开发中,这样的操作很容易被程序员不小心漏掉。
- 状态之间的切换关系,不过是往 buttonWasPressed 方法里堆砌 if、 else 语句,增加或者修改一个状态可能需要改变若干个操作,这使 buttonWasPressed 更加难以阅读和维护。
面向对象实现的状态模式
// 将状态封装成不同类
const weakLight = function(light) {
this.light = light
}
weakLight.prototype.press = function() {
console.log('打开强光')
this.light.setState(this.light.strongLight)
}
const strongLight = function(light) {
this.light = light
}
strongLight.prototype.press = function() {
console.log('关灯')
this.light.setState(this.light.offLight)
}
const offLight = function(light) {
this.light = light
}
offLight.prototype.press = function() {
console.log('打开弱光')
this.light.setState(this.light.weakLight)
}
const Light = function() {
this.weakLight = new weakLight(this)
this.strongLight = new strongLight(this)
this.offLight = new offLight(this)
this.currentState = this.offLight // 初始状态
}
Light.prototype.init = function() {
const btn = document.createElement('button')
btn.innerHTML = '按钮'
document.body.append(btn)
const self = this
btn.addEventListener('click', function() {
self.currentState.press()
})
}
Light.prototype.setState = function(state) { // 改变当前状态
this.currentState = state
}
const light = new Light()
light.init()
// 打开弱光
// 打开强光
// 关灯
非面向对象实现的状态模式
借助于 JavaScript 的委托机制, 可以像如下实现状态模式:
const obj = {
'weakLight': {
press: function() {
console.log('打开强光')
this.currentState = obj.strongLight
}
},
'strongLight': {
press: function() {
console.log('关灯')
this.currentState = obj.offLight
}
},
'offLight': {
press: function() {
console.log('打开弱光')
this.currentState = obj.weakLight
}
},
}
const Light = function() {
this.currentState = obj.offLight
}
Light.prototype.init = function() {
const btn = document.createElement('button')
btn.innerHTML = '按钮'
document.body.append(btn)
const self = this
btn.addEventListener('click', function() {
self.currentState.press.call(self) // 通过 call 完成委托
})
}
const light = new Light()
light.init()
或者通过闭包,创建一个状态机
// 生成一个状态机 参数为执行环境和状态对象
var delegate = function(client, delegation) {
return {
buttonWasPressed: function() { // 将客户的操作委托给 delegation 对象
return delegation.buttonWasPressed.apply(client, arguments);
}
}
};
var FSM = {
off: {
buttonWasPressed: function() {
console.log('关灯');
this.button.innerHTML = '下一次按我是开灯';
this.currState = this.onState;
}
},
on: {
buttonWasPressed: function() {
console.log('开灯');
this.button.innerHTML = '下一次按我是关灯';
this.currState = this.offState;
}
}
};
var Light = function() {
this.offState = delegate(this, FSM.off);
this.onState = delegate(this, FSM.on);
this.currState = this.offState; // 设置初始状态为关闭状态
this.button = null;
};
Light.prototype.init = function() {
var button = document.createElement('button'),
self = this;
button.innerHTML = '已关灯';
this.button = document.body.appendChild(button);
this.button.onclick = function() {
self.currState.buttonWasPressed();
}
};
var light = new Light();
light.init();
状态模式优缺点:
优点:
- 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
- 避免 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中原本过多的条件分支。
- 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
- Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
缺点:
- 在系统中定义许多状态类
- 由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。
状态模式中的性能优化点
-
两种选择来管理 state 对象的创建和销毁。第一种是仅当 state 对象被需要时才创建并随后销毁,另一种是**一开始就创建好所有的状态对象,并且始终不销毁它们。**如果 state对象比较庞大,可以用第一种方式来节省内存,这样可以避免创建一些不会用到的对象并及时地回收它们。但如果状态的改变很频繁,最好一开始就把这些 state 对象都创建出来,也没有必要销毁它们,因为可能很快将再次用到它们。
-
如果这些 state对象之间是可以共享的,各 Context 对象可以共享一个 state 对象,这也是享元模式的应用场景之一。
状态模式和策略模式的比较
状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为
相同点:
- 它们都有一个上下文(Context )、一些策略或者状态类,上下文把请求委托给这些类来执行。
区别:
策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;
而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。
总结:
状态模式也许是被大家低估的模式之一。实际上,通过状态模式重构代码之后,很多杂乱无章的代码会变得清晰。