前瞻
我们日常开始发时经常遇到复杂逻辑判断的情况,通常大家可以用 if-else 来实现多个条件判断。但随着逻辑复杂度的增加,代码中的 if-else 会变得越来越臃肿。那么如何更优雅的写判断逻辑,拒绝做一个 if-else 怪呢?以下是我的一些优化思想
举例
有个需求是这样的:马上中秋节要来了,我们本次商场要做大促销活动。对每个商品,我通过在给它设置不同的价格类型,让它展示不同的价格。产品订的逻辑是这样:
- 当价格类型为“预售价”时,打 9.5 折
- 当价格类型为“大促价”时,打 9 折
- 当价格类型为“返场价”时,打 8 折
- 当价格类型为“新人价”时,打 8 折
- 当价格类型为“尝鲜价”时,打 5 折
- 当价格类型为“正常价”时,原价
作为一名资深 if-else 怪,我们这样写:
/**
* @description 获取价格
* @param {*} tag 商品类型: pre 预售价 onSale 大促销 back 返场价 fresh 尝鲜价 normal 正常价 new 正常价
* @param {*} price 原价
* @return {*}
*/
function askPrice(tag, price) {
if (tag === 'pre') {
return price * 0.95
} else if (tag === 'onSale') {
return price * 0.9
} else if (tag === 'back') {
return price * 0.8
} else if (tag === 'new') {
return price * 0.8
} else if (tag === 'fresh') {
return price * 0.5
} else if (tag === 'normal') {
return price
}
}
除了 if-else 之外,大家也可以很轻易的提出这段代码的改写方案,switch:
function askPrice(tag, price) {
switch (tag) {
case 'pre':
return price * 0.95
case 'onSale':
return price * 0.9
case 'back':
case 'new':
return price * 0.8
case 'fresh':
return price * 0.5
case 'normal':
return price
}
}
这样看起来比 if-else 清晰多了,同时也将返场价格和新人价整合到了一起。
思考
上面的代码乍一看好像没什么毛病,但是仔细一看其实还是有一些优化的空间。
第一点:你有没有发现这个小小的函数里塞了6个逻辑。这会导致什么后果呢?如果一处逻辑错误,将会导致整个函数无法使用。这时候开发人员就需要一个个逻辑去查看。
第二点:如果产品又新增了一种 XX价,那是不是意味着又要加一重 if-else
第三点:尽管 if-else/swicth 就能完成效果,但是如果逻辑更加复杂不仅仅是打折还涉及到满减时。还继续使用 if-else 就会导致代码很冗余
针对以上几点最终可能会导致的后果就是,如果有BUG出现。开发人员要核对所有逻辑,测试人员也要所有逻辑都重新测。作为一个有追求的程序员,这样肯定是不行的。
解决方法
针对第一点,我们可以所有的逻辑都抽离出来:
function prePrice(price) {
// do sth
return price * 0.95
}
function onSalePcice(price) {
// do sth
return price * 0.9
}
function backPrice(price) {
// do sth
return price * 0.8
}
function newPrice(price) {
// do sth
return price * 0.8
}
function freshPrice(price) {
// do sth
return price * 0.5
}
function normalPrice(price) {
// do sth
return price
}
function askPrice(tag, price) {
if (tag === 'pre') {
return prePrice(price)
} else if (tag === 'onSale') {
return onSalePcice(price)
} else if (tag === 'back') {
return backPrice(price)
} else if (tag === 'new') {
return newPrice(price)
} else if (tag === 'fresh') {
return freshPrice(price)
} else if (tag === 'normal') {
return normalPrice(price)
}
}
改成这样之后,至少我们在遇到问题时就能快速定位到某个具体的函数。
写完之后,看代码我们可以发现。虽然将每个模块都抽离成一个方法,但是如果我们新增一个模块时,还是需要在 askPrice 函数里加 if-else。 那么有没有什么更好的方式改写 askPrice 函数,让我们在新增模块时,不用改 askPice 函数呢?
这时聪明的同学就会想到 对象映射:
const priceMap = {
pre(price) {
// do sth
return price * 0.95;
},
onSale(price) {
// do sth
return price * 0.9;
},
back(price) {
// do sth
return price * 0.8;
},
new(price) {
// do sth
return price * 0.8;
},
fresh(price) {
// do sth
return price * 0.5;
},
normal(price) {
// do sth
return price;
},
};
function askPrice(tag, price) {
return priceMap[tag](price)
}
是不是这样之后,我们增加新模块,就不需要去动 askPrice 函数,只需要去更改 priceMap 就可以了。同时还去除了 if-else。 整个代码看起来就更加的清爽优雅!
问题升级
原先的打折,力度还不够大。现在要加上满减,并且还增加一个VIP的角色,VIP 的优惠力度和普通用户不一样。
原谅我不仔细写每种类型的优惠情况,因为太多了。我们的目的也不是为了关注这个。
如果使用 if-else 的话代码是这样的:
/**
* @description 获取价格
* @param {*} tag 商品类型: pre 预售价 onSale 大促销 back 返场价 fresh 尝鲜价 normal 正常价 new 正常价
* @param {*} identity 身份: vip 高级用户 guest 普通用户
* @param {*} price 价格
*/
function askPrice(tag, identity, price) {
if (identity === 'guest') {
if (tag === 'pre') {
// do sth
} else if (tag === 'onSale') {
// do sth
} else if (tag === 'back') {
// do sth
} else if (tag === 'new') {
// do sth
} else if (tag === 'fresh') {
// do sth
} else if (tag === 'normal') {
// do sth
}
} else if (identity === 'vip') {
if (tag === 'pre') {
// do sth
} else if (tag === 'onSale') {
// do sth
} else if (tag === 'back') {
// do sth
} else if (tag === 'new') {
// do sth
} else if (tag === 'fresh') {
// do sth
} else if (tag === 'normal') {
// do sth
}
}
}
从上面的例子我们可以看到,当你的逻辑升级时,你的判断量会加倍,你的代码量也会加倍。所有我们还是需要使用到对象映射,来实现 askPrice 方法。
const priceMap = {
'vip_pre': () => {/* fn1 */ },
'vip_onSale': () => {/* fn2 */ },
'vip_back': () => {/* fn3*/ },
'vip_new': () => {/* fn4*/ },
'vip_fresh': () => {/* fn5 */ },
'vip_normal': () => {/* fn6 */ },
'guest_pre': () => {/* fn7 */ },
'guest_onSale': () => {/* fn8 */ },
'guest_back': () => {/* fn9 */ },
'guest_new': () => {/* fn10 */ },
'guest_fresh': () => {/* fn11 */ },
'guest_normal': () => {/* fn12 */ },
}
function askPrice(tag, identity, price) {
const onPrice = priceMap[`${identity}_${tag}`]
onPrice.call(this, price)
}
从上面的例子可以看出来,将有多个条件拼接成字符串,并通过以条件拼接字符串作为键,以处理函数作为值的对象进行查找并执行,这种写法在多元条件判断时候尤其好用。
需求总是多变的,这时候产品又改需求了,guest 用户的某几种类型,优惠力度改成一样。于是勤劳的程序员又开始改代码了。
写出来大概是这样:
const priceMap = {
'guest_pre': () => {/* fn1 */ },
'guest_onSale': () => {/* fn1 */ },
'guest_back': () => {/* fn1 */ },
'guest_new': () => {/* fn1 */ },
'guest_fresh': () => {/* fn11 */ },
'guest_normal': () => {/* fn12 */ },
// ...
}
这样写已经能满足日常需求了,但认真一点讲,上面重写了4次 fn1 还是有点不过优雅。那么有什么办法可以把这4个合成一个呢?也就是将这4个键合成一个呢?
聪明的我们这时候肯定能想到,既然要让键变化并且又是键值对的形式。那么 Map 对象就再合适不过了。
整理之后代码是这样:
const priceMap = () => {
const fn1 = () => {/*do sth*/ }
//...
return new Map([
[/^guest_(pre||onSale||back||new)$/, fn1],
[/^guest_fresh$/, fn2],
//...
])
}
function askPrice(tag, identity, price) {
let onPrice = [...priceMap()].filter(([key, value]) => key.test(`${identity}_${tag}`))
onPrice.forEach(([key, value]) => value.call(this, price))
}
充分 Map 的优点,使用正则作为 key。这里代码看起来是不是更加的高级,同时也能很好的应对更加复杂的判断。