1.状态模式
1.1 初识状态模式
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变
1.1.1 ’电灯程序‘例子
不同状态模式的实现代码
var Light = function(){
this.state = 'off'; // 给电灯设置初始状态 off
this.button = null; //电灯开关按钮
}
// 该方法负责在页面中创建一个真实的button节点,当button的onclick事件被触发的时候,就是电灯开关被按下的时候
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();
以上就是一个状态机。令人遗憾的是,电灯并非只有一种,比如有种电灯:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。必须得改造上面的代码
Light.prototype.buttonWasPressed = function(){
if (this.state === 'off') {
console.log('弱光');
this.state = 'weakLight'
} else if (this.state === 'weakLight') {
console.log('强光');
this.state = 'strongLight'
} else if (this.state === 'strongLight') {
console.log('关灯');
this.state = 'off'
}
}
以上代码的缺点:
-
buttonWasPressed方法违反开放-封闭原则的,每次新增或修改light的状态,都需要改动buttonWasPressed方法中的代码,这使得buttonWasPressed成为了一个非常不稳定的方法
-
所有跟状态有关的行为,都被封装在buttonWasPressed方法里,如果以后这个电灯又增加了强强光、超强光等,那将无法预计这个方法将膨胀到什么地步。
-
状态的切换非常不明显,仅仅表现为对state变量赋值,比如this.state = 'weakLight'。
-
状态之间的切换关系,不过是往buttonWasPressed方法里堆砌if、else语句,增加或者修改一个状态可能需要改变若干个操作,这使buttonWasPressed更加难以阅读和维护
1.1.2 状态模式改进电灯程序
谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以button被按下的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为
把状态的切换规则事先分布在状态类中,有效地消除了原本存在的大量条件分支语句
先定义3个状态类,分别是 OffLightState、WeakLightState、StrongLightState。这3个类都有一个原型方法buttonWasPressed,代表在各自状态下,按钮被按下时将发生的行为
// OffLightState
var OffLightState = function(light){
this.light = light
}
OffLightState.prototype.buttonWasPressed = function(){
console.log('弱光');
this.light.setState(this.light.weakLightState)
}
// WeakLightState
var WeakLightState = function(light){
this.light = light
}
WeakLightState.prototype.buttonWasPressed = function(){
console.log('强光');
this.light.setState(this.light.strongLightState)
}
// StrongLightState
var StrongLightState = function(light){
this.light = light
}
StrongLightState.prototype.buttonWasPressed = function(){
console.log('关光');
this.light.setState(this.light.offLightState)
}
改写Light类,不再使用一个字符串来记录当前的状态,而是使用更加立体化的状态对象。在Light类的构造函数里为每个状态类都创建一个状态对象,可以很明显地看到电灯一共有多少种状态
var Light = function(){
this.offLightState = new OffLightState(this)
this.weakLightState = new WeakLightState(this)
this.strongLightState = new StrongLightState(this)
this.button = null;
}
在button按钮被按下的事件里,Context也不再直接进行任何实质性的操作,而是通过self.currState.buttonWasPressed()将请求委托给当前持有的状态对象去执行,
Light.prototype.init = function(){
var button = document.createElement('button'),
self = this;
button.innerHTML = '开关';
this.button = document.body.appendChild(button);
this.currState = this.offLightState;
this.button.onclick = function(){
self.currState.buttonWasPressed();
}
}
状态对象通过这个方法来切换light对象的状态
Light.prototype.setState = function(newState){
this.currState = newState
}
var light = new Light();
light.init();
使用状态模式的好处很明显,它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码
2.状态模式的定义
定义: 允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类
对这句话的理解:
- 将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。
- 从客户的角度来看,使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果
3. 状态模式的优缺点
优点:
- 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换
- 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支
- 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然
- Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响
缺点:
- 会在系统中定义许多状态类,系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题。无法在一个地方就看出整个状态机的逻辑
4.状态模式中的性能优化
-
有两种选择来管理state对象的创建和销毁。第一种是仅当state对象被需要时才创建并随后销毁,另一种是一开始就创建好所有的状态对象,并且始终不销毁它们。如果state对象比较庞大,可以用第一种方式来节省内存,这样可以避免创建一些不会用到的对象并及时回收它们。但如果状态的改变很频繁,最好一开始就把这些state对象都创建出来,也没有必要销毁它们,因为可能很快将再次用到它们
-
为每个Context对象都创建了一组state对象,实际上这些state对象之间是可以共享的,各Context对象可以共享一个State对象,这也是享元模式的应用场景之一
5. 状态模式和策略模式的关系
它们都封装了一系列的算法或者行为,它们的类图看起来几乎一摸一样,但在意图上有很大的不同
相同点:都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行
它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态类和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在
6.JavaScript版本的状态机
前面的例子是模拟传统面向对象语言的状态模式实现,为每种状态都定义一个状态子类,然后在Context中持有这些状态对象的引用,以便把currState设置为当前的状态对象。
状态模式是状态机的实现之一,但在JavaScript这种“无类”语言中,没有规定让状态对象一定要从类中创建而来。另外一点,JavaScript可以非常方便地使用委托技术,并不需要事先让一个对象持有另一个对象。下面的状态机选择了通过Function.prototype.call方法直接把请求委托给某个字面量对象来执行
var Light = function(){
this.crttState = FSM.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.currState.buttonWasPressed.call(self);
}
var FSM = {
off: {
buttonWasPressed: function(){
console.log('关灯')
this.button.innerHTML = '下一次按我是开灯'
this.currState = FSM.on
}
},
on: {
buttonWasPressed: function(){
console.log('开灯')
this.button.innerHTML = '下一次按我是关灯'
this.currState = FSM.off
}
}
}
var light = new Light();
light.init();
另外一种方法,利用下面的delegate函数来完成这个状态机编写。这是面向对象设计和闭包互换的一个例子,前者把变量保存为对象的属性,而后者把变量封闭在闭包形成的环境中
var delegate = function(client, delegation) {
return {
buttonWasPressed: function(){
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.crttState = 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();
2.适配器模式
适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于不兼容而不能工作的两个软件实体可以一起工作。适配器的别名是包装器(wrapper)
2.1 例子
显示地图的方法不一致,一个叫“show”,一个叫“display”,用适配器处理成一致的
var googleMap = {
show: function(){
console.log('开始渲染谷歌地图')
}
}
var baiduMap: {
display: function(){
console.log('开始渲染百度地图')
}
}
var baiduMapAdapter = {
show: function(){
reutrn baiduMap.display()
}
}
renderMap(googleMap)
renderMap(baiduMapAdapter)
2.2 小结
有一些模式跟适配器模式的结构非常相似,比如装饰者模式、代理模式和外观模式。这几种模式都属于“包装模式”,都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图
- 适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使他们协同工作
- 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次
- 外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口