这是一篇,我自己都看不完的文章...
文章大体就两部分:
状态模式的介绍
状态机模型的函数库javascript-state-machine的用法和源码解析
场景及问题背景:
我们平时开发时本质上就是对应用程序的各种状态进行切换并作出相应处理。最直接的解决方案是将这些所有可能发生的情况全都考虑到,然后使用if... ellse语句来做状态判断来进行不同情况的处理。但是对复杂状态的判断就显得代码逻辑特别的乱。随着增加新的状态或者修改一个状态,if else或switch case语句就要相应的的增多或者修改,程序的可读性,扩展性就会变得很弱。维护也会很麻烦。
来个例子来,魂斗罗啊有木有玩过?:
先来看看最简单的一个动作的简单实现:
class Contra {
constructor () {
//存储当前待执行的动作
this.lastAct = {};
}
//执行动作
contraGo (act){
if(act === 'up'){
//向上跳
}else if(act === 'forward'){
//向前冲啊
}else if(act === 'backward'){
//往老家跑
}else if(act === 'down'){
//趴下
}else if(act === 'shoot'){
//开枪
}
this.lastAct = act;
}
};
var littlered = new Contra();
littlered.contraGo('shoot');
那要是有两个组合动作呢?改一下:
function contraGo (act){
constructor () {
//存储当前待执行的动作
this.lastAct1 = "";
this.lastAct2 = "";
}
contraGo (act1, act2){
const actArr = [act1, act2];
if(actArr.indexOf('shoot') !== -1 && actArr.indexOf('up') !== -1){
//跳着开枪吧
}else if(actArr.indexOf('shoot') !== -1 && actArr.indexOf('forward') !== -1){
//向前跑着开枪吧
}else if(actArr.indexOf('shoot') !== -1 && actArr.indexOf('down') !== -1){
//趴着开枪吧
}else if(actArr.indexOf('shoot') !== -1 && actArr.indexOf('backward') !== -1){
//回头跑着开枪吧
}else if(actArr.indexOf('up') !== -1 && actArr.indexOf('forward') !== -1){
//向前跳吧
}else if(actArr.indexOf('up') !== -1 && actArr.indexOf('down') !== -1){
//上上下下吧
}
...//等等组合
this.lastAct1 = act1;
this.lastAct2 = act2;
}
}
var littlered = new Contra();
littlered.contraGo('shoot');
缺点很明显了,大量的if else判断,加入哪天要给小红小蓝加一个回眸的动作,好嘛我又要修改contraGo方法,加一堆排列组合了,这使得contraGo成为了一个非常不稳定的方法,而且状态越多越庞大,升华一下,contraGo方法是违反开放-封闭原则的!
然后“状态模式”下凡来救人...
这里插一句开放封闭原则,到下一个分割线结束,大家可以跳一跳
有什么痛点 —— 开发过程中,因为变化、升级和维护等原因需要对原有逻辑进行修改时,很有可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有功能新测试。
怎么解决 —— 我们应该尽量通过扩展实体的行为来实现变化,而不是通过修改已有的代码来实现变化
具体一点呢 —— 类、模块和函数应该对扩展开放,对修改关闭。模块应该尽量在不修改原代码的情况下进行扩展。
核心 —— 用抽象构建框架,用实现扩展细节。
总结一下 —— 开发人员应该对程序中呈现的频繁变化的那些部分作出抽象,然后从抽象派生的实现类来进行扩展,当代码发生变化时,只需要根据需求重新开发一个实现类来就可以了。要求我们对需求的变更有一定的前瞻性和预见性,同时拒绝对于应用程序中的每个部分都刻意的进行抽象。
包含开闭原则在内,设计模式的六大原则,这里不详细介绍,简单列下:
单一原则 (SRP): 实现类要职责单一,一个类只做一件事或者一类事,不要将功能无法划分为一类的揉到一起,答应我好吗
里氏替换原则(LSP): 不要破坏继承体系,子类可以完全替换掉他们所继承的父类,可以理解为调用父类方法的地方换成子类也可以正常执行调用,爸爸打下的江山儿子继位得无压力好吗
依赖倒置原则(DIP):我说下我的理解,如果某套功能或者业务逻辑可能之后会出现并行的另外一种模式或者较大的调整,那不如把这部分逻辑抽象出来,创建一个包含相关方法的抽象类,而实现类继承这个抽象类来重写抽象类中的方法,完成具体的实现,调用这些功能方法的类不需要关心自己调用的这些个方法的具体实现,只管调用这些抽象类中定义好的形式上的方法即可,不与实际实现这些方法的类发生直接依赖关系,方便之后的实现逻辑的替换更改;
接口隔离原则(ISP) : 在设计抽象类的时候要精简单一,白话说就是,A需要依赖B提供的一些方法,A我只用B的3个方法,B就尽量不要给A用不到的方法啦;
迪米特法则(LoD)降低耦合,尽量减少对象之间的直接的交互,如果其中一个类需要调用另一个类的某一个方法的话,可通过一个关系类发起这个调用,这样一个模块修改时,就可以最大程度的减少波及。
开放-封闭原则(OCP)告诉我们要对扩展开放,对修改关闭,你可以继承扩展我所有的能力,到你手里你想咋改咋改,但是,别 动我 本人 好吗?好的
23种设计模式基于以上6大基本原则结合具体开发实践总结出来的,万变不离其宗,有了这些基本的意识规范,你写出来的,搞不好就是某种设计模式٩(๑>◡<๑)۶
解决方案
状态模式:允许一个对象在其内部状态改变的时候改变它的行为,对象看起来似乎修改了它的类。
先看下采用状态模式修改后的代码:
class Contra {
constructor () {
//存储当前待执行的动作 们
this._currentstate = {};
}
//添加动作
changeState (){
//清空当前的动作集合
this._currentstate = {};
//遍历添加动作
Object.keys(arguments).forEach(
(i) => this._currentstate[arguments[i]] = true
)
return this;
}
//执行动作
contraGo (){
//当前动作集合中的动作依次执行
Object.keys(this._currentstate).forEach(
(k) => Actions[k] && Actions[k].apply(this)
)
return this;
}
};
const Actions = {
up : function(){
//向上跳
console.log('up');
},
down : function(){
//趴下
console.log('do