浅析JavaScript状态模式及状态机模型、开放封闭原则的理解及使用、设计模式六大基本原则理解
一、场景及问题背景:
我们平时开发时本质上就是对应用程序的各种状态进行切换并作出相应处理。最直接的解决方案是将这些所有可能发生的情况全都考虑到,然后使用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方法是违反开放-封闭原则的!
解决方法:“状态模式”。
二、开放封闭原则介绍(以及设计模式6大原则)
1、有什么痛点?
开发过程中,因为变化、升级和维护等原因需要对原有逻辑进行修改时,很有可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有功能新测试。
2、怎么解决?
我们应该尽量通过扩展实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
3、具体一点呢?
类、模块和函数应该对扩展开放,对修改关闭。模块应该尽量在不修改原代码的情况下进行扩展。
4、核心:用抽象构建框架,用实现扩展细节。
5、总结一下:
开发人员应该对程序中呈现的频繁变化的那些部分作出抽象,然后从抽象派生的实现类来进行扩展,当代码发生变化时,只需要根据需求重新开发一个实现类来就可以了。
要求我们对需求的变更有一定的前瞻性和预见性,同时拒绝对于应用程序中的每个部分都刻意的进行抽象。
6、设计模式 6 大原则:
包含开闭原则在内,设计模式的六大原则,这里不详细介绍,简单列下:
(1)单一原则 (SRP):实现类要职责单一,一个类只做一件事或者一类事,不要将功能无法划分为一类的揉到一起,答应我好吗。
(2)里氏替换原则(LSP):不要破坏继承体系,子类可以完全替换掉他们所继承的父类,可以理解为调用父类方法的地方换成子类也可以正常执行调用,爸爸打下的江山儿子继位得无压力好吗
(3)依赖倒置原则(DIP):我说下我的理解,如果某套功能或者业务逻辑可能之后会出现并行的另外一种模式或者较大的调整,那不如把这部分逻辑抽象出来,创建一个包含相关方法的抽象类,而实现类继承这个抽象类来重写抽象类中的方法,完成具体的实现,调用这些功能方法的类不需要关心自己调用的这些个方法的具体实现,只管调用这些抽象类中定义好的形式上的方法即可,不与实际实现这些方法的类发生直接依赖关系,方便之后的实现逻辑的替换更改;
(4)接口隔离原则(ISP) : 在设计抽象类的时候要精简单一,白话说就是,A需要依赖B提供的一些方法,A我只用B的3个方法,B就尽量不要给A用不到的方法啦;
(5)迪米特法则(LoD):降低耦合,尽量减少对象之间的直接的交互,如果其中一个类需要调用另一个类的某一个方法的话,可通过一个关系类发起这个调用,这样一个模块修改时,就可以最大程度的减少波及。
(6)开放-封闭原则(OCP):告诉我们要对扩展开放,对修改关闭,你可以继承扩展我所有的能力,到你手里你想咋改咋改,但是,别动我本人好吗?
23 种设计模式基于以上6大基本原则结合具体开发实践总结出来的,万变不离其宗,有了这些基本的意识规范,你写出来的,搞不好就是某种设计模式。
三、解决方案
状态机模式:允许一个对象在其内部状态改变的时候改变它的行为,对象看起来似乎修改了它的类。
1、先看下采用状态模式修改后的代码:
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('down');
},
forward : function(){
//向前跑
console.log('forward');
},
backward : function(){
//往老家跑
console.log('backward');
},
shoot : function(){
//开枪吧
console.log('shoot');
},
};
var littlered = new Contra();
littlered.changeState('shoot','up').contraGo();
控制台会输出: shoot up
2、解决思路:
状态模式,将条件判断的结果转化为状态对象内部的状态(代码中的up,down,backward,forward),内部状态通常作为状态对象内部的私有变量(this._currentState),然后提供一个能够调用状态对象内部状态的接口方法对象(changeState,contraGo),这样对状态的改变,对状态方法的调用的修改和增加也会很容易,方便了对状态对象中内部状态的管理。
同时,状态模式将每一个条件分支放入一个独立的类中,也就是代码中的Actions。这使得你可以根据对象自身的情况将对象的状态(动作——up,down,backward,forward)作为一个对象(Actions.up,Actions.down这样),这一对象可以不依赖于其他对象而独立变化(一个行为一个动作,互不干扰)。
可以看出,状态模式就是一种适合多种状态场景下的设计模式,改写之后代码更加清晰,提高代码的维护性和扩展性,不用再牵一发动全身。
3、状态模式的使用场景:
(1)一个由一个或多个动态变化的属性导致发生不同行为的对象,在与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化,那么这个对象,就是有状态的对象
(2)代码中包含大量与对象状态有关的条件语句,像是if else或switch case语句,且这些条件执行与否依赖于该对象的状态。
如果场景符合上面两个条件,那我们就可以想象状态模式是不是可以帮忙了。
4、状态模式的优缺点:
优点:
- 一个状态对应行为,封装在一个类里,更直观清晰,增改方便
- 状态与状态间,行为与行为间彼此独立互不干扰
- 避免事物对象本身不断膨胀,条件判断语句过多
- 每次执行动作不用走很多不必要的判断语句,用哪个拿哪个
缺点:
- 需要将事物的不同状态以及对应的行为拆分出来,有时候会无法避免的拆分的很细,有的时候涉及业务逻辑,一个动作拆分出对应的两个状态,动作就拆不明白了,过度设计
- 必然会增加事物类和动作类的个数,有时候动作类再根据单一原则,按照功能拆成几个类,会反而使得代码混乱,可读性降低
还有其他介绍,如:
1、状态模式场景实例
2、有限状态机 Finite-state machine
3、有限状态机函数库Javascript Finite State Machine
可以看这篇文章讲的比较清晰:https://zhuanlan.zhihu.com/p/36556081