FSM状态机之状态模式
首先声明一点,这个模式是我目前见过最好用(本人观点),但是也是最难理解的一个(本人观点)。 所以大家需要做好心理准备,如果,对这个模式没有特别强烈的需求,比如: 我有一个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~.