《javascript设计模式与开发实践》阅读笔记(16)—— 状态模式
状态模式
会区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。比如电灯的开关是开还是关,在外界的表现就完全不同。
电灯例子
按照常规思路,实现一个电灯就是构造一个电灯类,然后指定一下它的开关是什么,每次开关改变,触发电灯相应的方法。
1 var Light = function(){
2 this.state = 'off'; // 给电灯设置初始状态off
3 this.button = null; // 尚未指定按钮
4 };
5
6 Light.prototype.init = function(){ //初始化,创造一个按钮给电灯对象
7 var button = document.createElement( 'button' ),
8 self = this; //保存引用
9 button.innerHTML = '开关';
10 this.button = document.body.appendChild( button );
11 this.button.onclick = function(){ //点一次开关调用一次对象的change方法
12 self.change();
13 }
14 };
15
16 Light.prototype.change = function(){
17 if ( this.state === 'off' ){
18 console.log( '开灯' );
19 this.state = 'on';
20 }else if ( this.state === 'on' ){
21 console.log( '关灯' );
22 this.state = 'off';
23 }
24 };
25
26 var light = new Light();
27 light.init();
这段代码是非常常规的实现,逻辑上也很容易理解,但有个小问题,所有的状态是写死在change方法里的,倘若我们想要添加一些状态就得深入进去修改,而且,想要知道对象一共有多少状态也得进去一个一个数,在代码量很多的情况下,change方法也会变得很臃肿。
引入模式方法前的思考
如果想要依次改变状态,而且可以知道一共有多少种状态,感觉通过数组完全可以实现,在light属性里定义一个状态数组,light的当前状态就是数组的第一个元素,每当点击时把状态改成数组的下一位。而且通过获取数组长度也很容易知道一共有多少状态,且能全部打印出来,修改起来也很方便。
1 var Light = function(){
2 this.stateArr=["off","on","small_light"]; //三个状态,关闭,点亮,暗一点
3 this.state = this.stateArr[0]; // 给电灯设置初始状态off
4 this.button = null; // 尚未指定按钮
5 };
6
7 Light.prototype.change=function(){
8
9 var num=this.stateArr.indexOf(this.state);
10 num=(num==this.stateArr.length-1)?0:num+1;
11 this.state=this.stateArr[num];
12 console.log( this.state );
13 }
改动之后每次点击依次触发 "on" "small_light" "off"。
不过现在还有一个问题,我们只是做到了可以依次改变状态和随便新增状态,但是change函数本质上也仅仅可以遍历状态而已,需要每种状态执行不同的函数时,现在的代码还是无能为力。我们希望的是,change函数遍历到对的状态,有一句通用的代码可以执行正确的函数。
所以,仅仅是把状态变成数组是不够的,我们需要的是一个对象,里面不但存储了状态,还应该存储了相应方法。我们统一这些方法的名称,在change函数里就可以统一调用。
状态模式实现
1 /****新建off类***/
2 var off=function(){
3 this.statename="off";
4 }
5 off.prototype.do=function(){
6 console.log("关灯啦");
7 }
8
9 /****新建on类****/
10 var on=function(){
11 this.statename="on";
12 }
13 on.prototype.do=function(){
14 console.log("开灯啦");
15 }
16
17 /****新建small_light类****/
18 var small_light=function(){
19 this.statename="small_light";
20 }
21 small_light.prototype.do=function(){
22 console.log("光线变暗啦");
23 }
24
25 var Light = function(){
26 this.stateArr=[]; //存储状态对象
27 this.stateArr.push(new off()); //添加状态对象,如果要增删或者调整顺序,通过操作数组或者这里手动修改都可以做到
28 this.stateArr.push(new on());
29 this.stateArr.push(new small_light());
30
31 this.stateobj=this.stateArr[0]; //指定初始状态对象
32 this.state = this.stateobj.statename; // 给电灯设置初始状态
33 this.button = null; // 尚未指定按钮
34 };
35
36 Light.prototype.init = function(){ //初始化,创造一个按钮给电灯对象
37 var button = document.createElement( 'button' ),
38 self = this;
39 button.innerHTML = '开关';
40 this.button = document.body.appendChild( button );
41 this.button.onclick = function(){ //点一次开关调用一次对象的change方法
42 self.change();
43 }
44 };
45
46 Light.prototype.change = function(){
47 var num=this.stateArr.indexOf(this.stateobj); //返回当前状态对象在数组的索引
48 num=(num==this.stateArr.length-1)?0:num+1; //根据索引做些处理
49
50 this.stateobj=this.stateArr[num]; //重新指定状态对象
51 this.state=this.stateobj.statename; //改变状态
52 this.stateobj.do(); //执行新状态对象的代码
53 };
54
55 var light = new Light();
56 light.init();
57
58 /****点击按钮执行结果***/
59 // 开灯啦
60 // 光线变暗啦
61 // 关灯啦
62 // 开灯啦
63 //....(不断重复)
上述代码可以自行去浏览器的控制台测试,这里还有一点小问题,就是接口的统一(do方法)完全要靠程序员的自觉和记忆力,有的时候,如果在代码较多的时候,因为没有接口统一引起bug,调试起来很麻烦,所以我们想到了模板方法中为了避免这种情况的做法。让所有状态类都产生于一个抽象类,抽象类的do方法中抛出一个错误,如果状态类没有重写该方法,在程序执行后也能很快发现错误的原因。
状态模式的优缺点
优点:
状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
避免Context(上下文)无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
缺点:
是会在系统中定义许多状态类,编写多个状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。
总结
这里因为例子如此,所以用了数组来管理,而实际的开发中,会出现各种各样切换状态的方式,不一定是顺序的;而且可能存在两个或者更多按钮,他们的点击都会导致状态变化,这种时候,就需要根据具体的情况,完成状态的切换和执行相应代码。