5

 首先声明一点,这个模式是我目前见过最好用(本人观点),但是也是最难理解的一个(本人观点)。 所以大家需要做好心理准备,如果,对这个模式没有特别强烈的需求,比如: 我有一个Button,我按次数点击它,他会触发不同的状态 等等这样的,可以学习一下其他的模式。但是!!! 如果你看了我这篇文章,被我前面说的话吓到了,那么就继续往下看,其实,状态模式是最好用,也是最容易掌握的一个。

大话状态模式

上面已经提到了,状态模式其实就是,一个事物的内部状态的改变,产生不同的行为。 要注意这里的

"一个","状态","行为". 因为这3个词是状态模式中最重要的3个概念。

同样,举个栗子

大家家里都有空调吧,remoter应该都用过。 首先我们拿一个简单的使唤。 就拿 on/off 键吧。首先,遥控器会记录当前的状态(假设他会这样),如果是on, 当你点击 他会发出off的信号,如果你是off,则会发出on的信号.我们用程序说明一下.

var switches = (function(){
    var state = "off";
    return function(){
        if(state === "off"){
            console.log("打开空调");
            state = "on";
        }else if(state === "on"){
            console.log("关闭空调");
            state = "off";
        }
    }
})();
document.querySelector(".switch").addEventListener("click",function(){
    switches();  //模仿你打开/关闭空调的状态
},false)

很简单吧,一个闭包+一个变量 就可以构成一个 状态机,是不是超级神奇呢?
恩,看到这里,聪明的人会,会心一笑,然后继续往下看。
是个屁。
想想,如果空调开关要是都只有两种状态,尼玛谁用啊!!!
满是turn on/off. 就跟写2进制一样。实话说,哥汇编学的差,所以也十分不愿意直视二进制,你让我使唤开关跟写汇编似的,操。 0001是开,0000是关,0010是加热模式,0011是制冷模式。

所以,海尔,美的考虑到国情,制造了比较人性化的remoter。
现在,如果我们使用上面那种模式来写,切换模式的switch。

var switches = (function(){  //auto->hot->cold->wind->dry->auto
    var state = "auto";
    return function(){
        if(state === "auto"){
            console.log("制热");
            state = "hot";
        }else if(state === "hot"){
            console.log("制冷");
            state = "cold";
        }else if(state === "cold"){
            console.log("送风");
            state = "wind";
        }else if(state === "wind"){
            console.log("除湿");
            state = "dry";
        }else if(state === "dry"){
            console.log("自动");
            state = "auto";
        }
    }
})();
document.querySelector(".switch").addEventListener("click",function(){
    switches();  //模仿你切换空调的模式
},false);

呵呵呵~ 功能是实现了,不过代码,又被if语句给rape的。 性能的强奸犯,阅读的杀手 恐怕就算if语句了. 所以,为了不犯罪,我们需要优化我们的状态模式。

高级状态模式

其实,这个状态模式的写法和命令模式有着异曲同工的妙处。即,中间有个状态仓库,然后分别将命令转发给对应的执行类。
总结一下。 高级状态模式需要有,状态仓库,状态类,状态执行者,这3个要点。 对应着我们的,”一个“,"状态","行为". 一个仓库,不同的状态,不同的执行。
just do it.

//定义状态
var Auto= function(button){
    this.turn = button;
}
Auto.prototype.press= function(){
    console.log('制热');
    this.turn.setState("hot");
}
var Hot = function(button){
    this.turn = button;
}
Hot.prototype.press= function(){
    console.log('制冷');
    this.turn.setState("cold");
}
var Cold = function(button){
    this.turn = button;
}
Cold.prototype.press= function(){
    console.log('送风');
    this.turn.setState("wind");
}
var Wind = function(button){
    this.turn = button;
}
Wind.prototype.press= function(){
    console.log('除湿');
    this.turn.setState("dry");
}
var Dry = function(button){
    this.turn = button;
}
Dry.prototype.press= function(){
    console.log('自动');
    this.turn.setState("auto");
}
//定义状态仓库
var Remoter = function(){
    this.auto = new Auto(this);
    this.hot = new Hot(this);
    this.cold = new Cold(this);
    this.wind = new Wind(this);
    this.dry = new Dry(this);
    this.state = "auto";
}
Remoter.prototype.setState = function(state){
    this.state=state;
}
Remoter.prototype.press = function(){
    this[this.state].press();  //执行对应状态的press
}
Remoter.prototype.init = function(){  //定义执行者
    document.querySelector('.switch').addEventListener("click",()=>{
        this.press();
    },false);
}
new Remoter.init();  //初始化

上面那种就是一个比较模式化的写法,而且,可复用,可添加。 比上面那种的逼格不知道高到哪里去,但是,实现成本也是挺大的。考虑到这点,聪明的ECMA-262在es6中推出了状态机这个伪函数,能够帮助我们快速实现状态化。
Duang~
就是generator函数。 目前FF,edge,chrome 最新版本已经支持。不过可以使用babel进行转化.
我们使用generator进行重构.
我比较懒,我们就先实现前3个模式的转化吧。

var auto = function(){
    console.log("自动");
}
var hot = function(){
    console.log("制热");
}
var cold = function(){
    console.log("制冷");
}
function* models(){
    for(var i = 0,fn,len=arguments.length;fn = arguments[i++];){
        yield fn();
        if(i===len){
            i = 0;
        }
    }
}
var exe = models(auto,hot,cold);  //按照模式顺序排放
document.querySelector(".switch").addEventListener("click",function(){
    exe.next();
},false);

已经没有了if来进行分支判断,效果也是蛮不错的。 关于generator的用法,还有进程控制,这些都是比较高级的用法,有兴趣的同学可以参考 阮老师的 es6讲解. 但是,推荐还是使用,一个仓库,不同状态,不同行为,这样函数对象式的写法,扩展性比较强。主要原因是因为,generator还未普及,以及设置他进程的顺序比较复杂。不过,平常本人喜欢装装逼,永远热爱新技术,所以大部分时候还是会使用generator。 总之,程序员并不是程序员,我们要有自己的核心价值观,找到自己最对的 "玛卡瑞纳",这才是我们程序员应该有的情怀。
我们仔细观察一下上面使用"类"写出来的状态模式,会发现,状态类是不是感觉可以使用享元模式优化呢?没错。因为他的方法和状态都是一致的,当然可以使用。

var obj = {
    auto(){
        console.log("自动")
        return "hot";
    },
    hot(){
        console.log("制热");
        return "cold";
    },
    cold(){
        console.log("制冷");
        return "auto"
    }
}
var State = function(){
    this.state = "auto";
    this.obj = obj;
}
State.prototype.next = function(){
    this.state = this.obj[this.state]()
}
new State().next();  //测试通过.

这只是一种比较轻巧的方法,js,最出名的就是他的动态,无拘无束,你可以天马行空的写出你的代码(但是,必须保证的你代码不会变成 凤姐 ).
但从上面的代码可以看出,如果程序里面使用return 的话,很容易会造成你函数的逻辑复杂度,所以我们这里推荐使用一个state进行保存,将this.state传入。 当然,我们并不是当参数参入了(太low),我们使用委托的技术传入,相当于给this动态织入一个函数。这个方法就叫: apply和call. 哈哈,是不是有种感觉(怎么又是你).

var obj = {
    auto(){
        console.log("自动")
        this.state = "hot";
    },
    hot(){
        console.log("制热");
        this.state = "cold";
    },
    cold(){
        console.log("制冷");
        this.state = "auto";
    }
}
var State = function(){
    this.state = "auto";
    this.obj = obj;
}
State.prototype.next = function(){
    this.obj[this.state].call(this);
}
new State().next();

没错,这下,我们不仅能将函数动态织入,而且可以直接改动state,这样可以给自己程序的扩展性加上一分。
当然,状态模式的写法还有很多,比如delegate函数的写法等等。 不过,找到自己的"玛卡瑞纳"才是最棒的。
上面只是一个线上的流式状态切换,并没有涉及很复杂的业务逻辑。但是,如果你在开发一个大型项目的时候,涉及的状态可谓是五花八门,还是以空调遥控器为例,比如,你切换到模式选择的时候,你的上下左右键,只能控制模式的切换,而不能控制风速大小,当你切换到风速选择模式的时候,同样不能控制其他的功能。 所以,如果按照上面那种 单线式的状态切换是不够的。 这里就引入了FsM(finite-state-machine),状态机这个概念,以及和他对应的状态表。
如下图

如果你是学机械的,那么这个状态切换的概念应该非常熟悉,在CH40161(一种自触发式芯片)中,你输入一个触发信号,他可以按照你这个触发信号逐步触发(我机械太渣,但意外的喜欢上计院). 在js中,gordon大神(有8个contributor)已经写出了这个状态库。有兴趣的同学可以看一看。
传送门: FSM
其实,他里面最重要的就是"状态"和"状态切换"的规则。
先看一个demo:

var fsm = StateMachine.create({
  initial: 'green',
  events: [
    { name: 'warn',  from: 'green',  to: 'yellow' },
    { name: 'panic', from: 'yellow', to: 'red'    },
    { name: 'calm',  from: 'red',    to: 'yellow' },
    { name: 'clear', from: 'yellow', to: 'green'  }
  ],
  callbacks: {
    onpanic:  function(event, from, to, msg) { alert('panic! ' + msg);               },
    onclear:  function(event, from, to, msg) { alert('thanks to ' + msg);            },
    ongreen:  function(event, from, to)      { document.body.className = 'green';    },
    onyellow: function(event, from, to)      { document.body.className = 'yellow';   },
    onred:    function(event, from, to)      { document.body.className = 'red';      },
  }
});

这已经定义好了一个完整的单线式,状态切换队列。
当你触发fsm.warn(); 状态就是从green->yellow。
当你触发fsm.panic(); 状态就是从yellow->red.
...
说一下基本用法
events 里面就是你定义的状态表的规则

name: 标识,状态切换的函数名
from: 标识 为切换之前的状态
to: 标识 为切换之后的状态

callbacks 里面就是对状态和切换规则函数的定义. 这里不说的太复杂,就按照基本的讲解吧。


使用on+Name; 定义状态切换的函数
使用on+State: 定义某个状态时触发的函数

当然,还有

onbeforeevent - fired before any event
onleavestate - fired when leaving any state
onenterstate - fired when entering any state
onafterevent - fired after any event

这些比较细,这里就不做详细介绍,如果有兴趣的同学可以去github上面看一看,理解起来也不是很难。我这里介绍的我经常使用的。
所以,上面的流程就是。
使用fsm.panic() 之后。
触发顺序为: onpanic()->red();
如果你状态不对,而强行调用fsm.panic的话就会触发error函数(这里没有写). 所以,上面写的fsm 差不多已经够用了,关键看你如果组合了。 要知道,二维难度 >> 一维难度。 有一个好工具,能把你的工作量降到最低。

谈谈状态模式

说到这里,我的这篇blog大部分是介绍 一些基本原理和方法,状态模式的应用在程序设计中是非常重要的一个概念,如果你掌握了,语言只会变为你的一个工具,因为 你已经吃透了 隐藏在 语言背后的 secret. 最后还是那句话, 不要为了模式而模式,但状态模式确实是个好模式。
ending~.


villainhr
7.8k 声望2.2k 粉丝