分支语句优化--状态模式
发表日期:2019-01-25
状态模式是一种可以极大程度上简化分支语句的行为型模式。如果代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态。这里状态可以是OA系统中批文的流转状态,可以是订单正逆向交易过程中的各种中态,应用场景十分常见。我们可以这样定义状态模式:
对象的行为依赖于它的状态(属性),并且可以根据它状态的改变产生不同的行为,从外部看好像是改变了这个对象。
下面是我们常见的分支语句。对于复合状态的条件判断,其开销是翻倍的,十分不利于维护修改。
// 单状态条件判断 每增加一个状态就需要添加一个判断
function Foo1 (status) {
if(status === 1) {
// do sth1
} else if(status === 2) {
// do sth2
}else {
// do sth3
}
}
// 复合状态对条件判断的开销是翻倍的
function Foo2 (status1, status2) {
if (status1 === 1) {
// do sth1
} else if (status1 === 2) {
// do sth2
} else if (status1 === 1 && status2 === 2) {
// do sth1 and sth2
} else if (status1 === 2 && status2 == 3) {
// do sth2 and sth3
}
}注意上边复合状态所产生的行为是单个状态所产生行为的加总。下面我们用状态模式来实现多分支的逻辑。
模式实例
// 创建状态类
function Foo3 () {
// 安全模式
if (!(this instanceof Foo3)) return new Foo3()
// 内部状态私有变量
var _currentStates = {},
// 状态枚举容器,当然此处可以使用对象来代替
states = new Map([
[1, () => {// do sth1}],
[2, () => {// do sth2}],
[3, () => {// do sth3}],
[4, () => {// do sth4}],
[5, () => {// do sth5}],
['default', () => {// do sth6}]
])
// 状态控制类
var Action = {
// 改变状态方法 通过传递多个状态决定执行多个动作
changeState: function (...args) {
// 重置内部状态
_currentstate = args || [];
// 返回动作控制类
return this;
},
// 执行状态对应的动作
goes: function () {
// 遍历内部保存的状态
for (let i of _currentstate) {
// 如果该状态存在则执行
let action = states.get(i) || states.get('default')
action.call(this)
}
return this;
}
}
// 返回接口方法 change、goes
return {
change: Action.changeState,
goes: Action.goes
}
}状态类内部除了有一个私有的状态变量,还有一个状态枚举容器,用来定义所有单一状态值的行为。这里我们使用了ES6的map,当然此你可以使用对象来代替,但是之后你会看到使用map这个新语法特性所带来的巨大灵活性。
由于状态类内部需要保存当前状态,必须实例化为不同的对象使用,所以在其构造方法中加入了安全模式,即使调用方使用Foo3()初始化,也相当于使用了new关键字new Foo3()。
这里我们输出了两个接口,分别是change和goes,用于改变当前实例的内部状态和执行实例中的所有状态。这样对于臃肿耦合的分支处理逻辑,便转换成以下更为清晰独立的调用方式。
Foo3().change(1, 2).goes().change(3).goes(); // do sth1, do sth2, do sth3下面我们来升级一下复合状态的复杂度,我们让多个状态所产生的行为,不再是单个状态所产生行为的简单加总了。此时应该怎么做呢?
function Foo4 (status1, status2) {
if (status2 === 'A') {
if (status1 === 1) {
// do sth1
} else if (status1 === 2) {
// do sth2
}
// ...
} else if (status2 === 'B') {
if (status1 === 1) {
// do sth3
} else if (status1 === 2) {
// do sth4
}
// ...
}
}
function Foo5 () {
if (!(this instanceof Foo5)) return new Foo5()
var _currentStates = {},
states = new Map([
['A_1', () => {// do sth1}],
['A_2', () => {// do sth2}],
['B_1', () => {// do sth3}],
['B_2', () => {// do sth4}],
// ...
['default', () => {// do sth5}]
])
var Action = {
changeState: function (...args) {
_currentstate = args || [];
return this;
},
goes: function () {
let action = states.get(`${_currentstate[2]}_${_currentstate[1]}`) || states.get('default')
action.call(this)
return this;
}
}
return {
change: Action.changeState,
goes: Action.goes
}
}Foo5修改的地方主要是状态枚举的map和输出的goes接口,这里巧妙的用多状态值的字符串拼接,作为map的键值,进而可以通过以下方式调用。
Foo4().change(1, ‘A’).goes(); // do sth1取巧Map
接下来我们再来升级一下复杂度,证明一下map而不是object的巧妙之处。如果状态A下有多种复合状态是做相同的事,其他情况做不同的事,并且A状态下的所有复合状态都需要执行一段相同的逻辑,按照前述做法,states应该是这样的:
states = new Map([
['A_1', () => {// do sth1}],
['A_2', () => {// do sth1}],
['A_3', () => {// do sth1}],
['A_4', () => {// do sth2}],
// ...
['common', () => {// do common thing}]
])但如果状态规模足够大,或者键值之间的关系更加复杂多变时,map中穷举这样的实现方式也和if...else分支一样略显粗鄙。先来了解一下map与object的巨大差异。
一个
Map的键可以是任意值,包括函数、对象、基本类型、正则。
Map 中的键值是有序的,而添加到对象中的键则不是。因此,当对它进行遍历时,Map 对象是按插入的顺序返回键值。
你可以通过
size属性直接获取一个Map的键值对个数,而Object的键值对个数只能手动计算。
Map可直接进行迭代,而Object的迭代需要先获取它的键数组,然后再进行迭代。
Object都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。虽然 ES5 开始可以用map = Object.create(null)来创建一个没有原型的对象,但是这种用法不太常见。
Map在涉及频繁增删键值对的场景下会有些性能优势。
以上摘自MDN,我们这里用到的特性是map键可以为正则,正则的灵活性可以充分简化绝大部分复杂多变的状态关系,如下:
states = new Map([
[/^A_[1-3]$/, () => {// do sth1}],
[/^A_4$/, () => {// do sth1}],
// ...
[/^A_.*$/, () => {// do common thing}]
])
goes: function () {
// 注意无法用 Map.prototype.get(key) 来获取key为正则的值,只能遍历过滤
let states_post = [...states].filter(([key, value]) => key.test(`${_currentstate[2]}_${_currentstate[1]}`))
states_post.forEach([key, value] => value.call(this))
return this;
}相信这个例子足以在今后处理复杂分支问题上,带给你诸多灵感。
小结
状态模式可以解决程序中臃肿的分支判断语句问题,将每个分支转化为一种状态独立出来,方便每种状态的管理又不至于每次执行时遍历所有分支。再程序中到底产出哪种行为结果,决定于选择哪种状态,而选择何种状态又是再程序运行时决定的。当然状态模式最终的目的是简化分支判断流程。
参考文献
JavaScript设计模式 张容铭
最后更新于