前言
相信很多人有这样的经历,在项目比较忙的时候,都是先考虑实现,用当时以为最好的方式先实现方案,在项目不忙的时候,再看下以前代码,想下有什么更好的实现方案,或者优化方案。例如,当我们遇到需要多个条件判断的业务操作时,最简单直接的办法就是用多个if-else判断,虽然能够实现,但当判断条件过多,甚至是多元判断时,我们的代码就会变得非常不好阅读和维护。下面就和大家分享一下自己常用的在特定场合下,代替if-else,switch的解决方案。如果大家有什么想法,欢迎提出来,多多交流。
举个例子
- 需求: 写一个 returnWeekday() 方法返回"今天是星期几"。
当我们开始拿到需求的时候,看到一系列的逻辑判断,首先想到的应该就是 if 语句了。( 这里也可以用switch-case )
function returnWeekday() {
let string = '今天是星期'
let date = new Date().getDay()
if (date === 0) {
string += '日'
} else if (date === 1) {
string += '一'
} else if (date === 2) {
string += '二'
} else if (date === 3) {
string += '三'
} else if (date === 4) {
string += '四'
} else if (date === 5) {
string += '五'
} else if (date === 6) {
string += '六'
}
return string
}
console.log(returnWeekday())
当我们写完了这样的代码,第一感觉就是 if else 块是不是太多了。
假设哪一天,需求变了,我们这个 returnWeekday() 方法就需要多加一层判断了。
我们的希望是已经封装好的方法,不要频繁的修改。可是需求的变动是你无法控制的。
所以我们继续思考该怎么优化。
- 初步尝试
我们看到这里的 case 是数字,和数组的下标是一致的。所以我们可以考虑使用数组来优化。
function returnWeekday() {
let str = '今天是星期'
let date = new Date().getDay()
// 使用数组
let dateArr = [天, 一, 二, 三, 四, 五, 六]
return str + dateArr[date]
}
console.log(returnWeekday())
以上代码是不是比 switch 语句和 if 语句清晰多了。而且就算一周变为八天,只需要修改 dateArr 数组就好了。
- 再次尝试
需要返回的是一行文字,那我们是不是可以使用字符串的方法去实现(字符串有个和使用数组下标类似的方法)
// charAt 定位方法
function returnWeekday() {
return '今天是星期' + '日一二三四五六'.charAt(new Date().getDay())
}
console.log(returnWeekday())
以上是的需求刚好是每个条件都有规律的数字,如果我们每个条件都是不规律的字符串呢?
举个例子
if (status === '1') {
return '未审批'
} else if (status === '2') {
return '审批中'
} else if (status === '3') {
return '审批通过'
} else if (status === '4') {
return '审批退回'
} else if (status === '5') {
return '审批异常'
}
- 使用Object对象
适用于单层多个判断
这是一种比较常见的方式,用Object对象把判断收集到一起
const statusArr = {
'1': '未审批',
'2': '审批中',
'3': '审批通过',
'4': '审批退回',
'5': '审批异常',
}
function getStatus(Num) {
return statusArr[Num]
}
getStatus('1') //未审批
这样是不是变得优雅一些了呢?可能看下来可能优化的效果没有非常显著,但是将逻辑通过数据的形式维护起来,可读性更好。
以上是单层多个判断,如果我们加一层呢?
-
比如当我们加上一个角色判断时
if (role === '打工人') { if (status === '1') { //一些操作 } else if (status === '2') { //一些操作 } else if (status === '3') { //一些操作 } else if (status === '4') { //一些操作 } else if (status === '5') { //一些操作 } } else if (role === '老板') { if (status === '1') { //一些操作 } else if (status === '2') { //一些操作 } else if (status === '3') { //一些操作 } else if (status === '4') { //一些操作 } else if (status === '5') { //一些操作 } }
可以看到,当同一个方法,不同角色不同状态执行的操作不同时,代码将会变得冗长。
优化的方法:同样使用一个对象收集这种不同的状态,但是由于多元的关系,将角色和状态进行字符串的拼接。
let statusArr = {
'打工人_1': ()=>{ /*一些操作*/ },
'打工人_2': ()=>{ /*一些操作*/ },
'打工人_3': ()=>{ /*一些操作*/ },
'打工人_4': ()=>{ /*一些操作*/ },
'打工人_5': ()=>{ /*一些操作*/ },
'老板_1': ()=>{ /*一些操作*/ },
'老板_2': ()=>{ /*一些操作*/ },
'老板_3': ()=>{ /*一些操作*/ },
'老板_4': ()=>{ /*一些操作*/ },
'老板_5': ()=>{ /*一些操作*/ },
}
function getStatus(role,status) {
return statusArr[`${role}_${status}`]
}
可以看到,改造之后,更加简明易读。不过个人认为这种拼接的方式始终不够规范,易读很可能只针对自己,对于接手的可能语义化还不够,下面介绍一种更好的方式。
- 使用对象的方式存放在Map对象上
上面这种方式之所以要拼接在一起,是因为对象的键需要是字符串的限制,如果使用Map对象,键就可以是一个对象、数组或者更多类型,方便了很多。(Map对象文档)
let statusMap = new Map([
[{role: '打工人', status : '1'}, ()=>{ /*一些操作*/ }],
[{role: '打工人', status : '2'}, ()=>{ /*一些操作*/ }],
[{role: '打工人', status : '3'}, ()=>{ /*一些操作*/ }],
[{role: '打工人', status : '4'}, ()=>{ /*一些操作*/ }],
[{role: '打工人', status : '5'}, ()=>{ /*一些操作*/ }],
[{role: '老板', status : '1'}, ()=>{ /*一些操作*/ }],
[{role: '老板', status : '2'}, ()=>{ /*一些操作*/ }],
[{role: '老板', status : '3'}, ()=>{ /*一些操作*/ }],
[{role: '老板', status : '4'}, ()=>{ /*一些操作*/ }],
[{role: '老板', status : '5'}, ()=>{ /*一些操作*/ }],
])
let getStatus = function(role,status) {
statusMap.forEach((value,key)=>{
if(JSON.stringify(key) == JSON.stringify({role: role,status: status})){
value()
}
})
}
getStatus('打工人','1') // 一些操作
解释一下这段代码:
将状态和角色组成对象放入Map对象中作为键名,将不同的方法作为对应的值。getStatus方法找到对应的操作并执行
这里有一个小坑,由于键名是对象,所以查找Map对象时不能用Map.get()的方法,原因是由于对象的引用类型的问题,所以我用了这种方式转成字符串来比较。实测没有问题。
- 假如打工人情况下,status1-4的处理逻辑都一样怎么办
可以尝试以下:
let statusMap = new Map([
[{role: '打工人', status : '1'}, ()=>{ /* 事件A */ }],
[{role: '打工人', status : '2'}, ()=>{ /* 事件A */ }],
[{role: '打工人', status : '3'}, ()=>{ /* 事件A */ }],
[{role: '打工人', status : '4'}, ()=>{ /* 事件A */ }],
[{role: '打工人', status : '5'}, ()=>{ /* 事件B */ }],
])
这样写已经能满足日常需求了,但认真一点讲,上面重写了4次事件A还是有点不爽,假如判断条件变得特别复杂,比如role有3种状态,status有10种状态,那你需要定义30条处理逻辑,而往往这些逻辑里面很多都是相同的,那可以这样实现:
let statusMap = new Map([
[/^打工人_[1-4]$/, ()=>{ /* 事件A */ }],
[/^打工人_5$/,()=>{ /* 事件B */ }],
])
let getStatus = function (role, status) {
statusMap2.forEach((value, key) => {
if (key.test(`${role}_${status}`)) {
return value();
}
// key.test(`${role}_${status}`) && value();
});
};
getStatus("打工人", "1");
也就是说利用数组循环的特性,符合正则条件的逻辑都会被执行,那就可以同时执行公共逻辑和单独逻辑,因为正则的存在,你可以打开想象力解锁更多的玩法,本文就不赘述了。
以上几种都是基于处理逻辑和配置数据分离的优化方法,这种写法也叫 表驱动法 或者 策略模式,这些方法,适用于一些逻辑不是太复杂的处理,对于一些复杂的处理逻辑,可以考虑使用责任链模式。
责任链模式
- 什么是责任链模式
责任链模式就是某个请求需要多个对象进行处理,从而避免请求的发送者和接收之间的耦合关系。将这些对象连成一条链子,并沿着这条链子传递该请求,直到有对象处理它为止。
责任链模式中的角色:发送者、接受者
责任链模式的流程:
-
发送者知道链中的第一个接受者,它向这个接受者发出请求
-
每一个接受者都对请求进行分析,要么处理它,要么往下传递
-
每一个接受者知道的其他对象只有一个,即它的下家对象
-
如果没有任何接受者处理请求,那么请求将从链上离开,不同的实现对此有不同的反应
- 举个例子
需求: 公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。
- orderType:表示订单类型(定金用户或者普通购买用户),code 的值 为 1 的时候是 500 元定金用户,为 2 的时候是 200 元定金用户,为 3 的时候是普通购买用户。
- pay:表示用户是否已经支付定金,值为 true 或者 false, 虽然用户已经下过 500 元定金的订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式。
- stock: 表示当前用于普通购买的手机库存数量,已经支付 过 500 元或者 200元定金的用户不受此限制。
先实现下这个需求
let order = function (orderType, pay, stock) {
if (orderType === 1) {
if (pay === true) {
console.log("500元定金,得到100元优惠券");
} else {
//未支付定金,降级到普通购买模式
if (stock > 0) {
//用于普通购买的手机还有库存
console.log("普通购买,没有优惠券");
} else {
console.log("手机库存不足");
}
}
} else if (orderType === 2) {
if (pay === true) {
console.log("200元定金预约,得到50优惠券");
} else {
if (stock > 0) {
console.log("普通购买,没有优惠券");
} else {
console.log("手机库存不足");
}
}
} else if (orderType === 3) {
if (stock > 0) {
console.log("普通购买,没有优惠券");
} else {
console.log("手机库存不足");
}
}
};
order(1, true, 500); // 500元定金,得到100元优惠券
虽然我们得到了意料中的运行结果,但这远远算不上一段值得夸奖的代码。虽然目前项目能正常运行,如果中间增加一个新的优惠逻辑,那么你就要修改这个庞大的 order方法,维护工作变得复杂。
小结
字数限制,请看下一篇 浅析-特定场景下if-else的优化方案(下)