我不喜欢写 switch-case
语句,虽然相比于 Java 来说,Javascript 的 switch-case
要更为强大,但还是无法避免该结构固有的缺陷。同样的情况发生自 if-else
结构,当分支变得复杂之后,编写出来的代码在阅读上简直就是灾难。
条件分支语句的困境
首先需要明确对于复杂的条件分支语句的短板到底在哪里:
- 阅读困难
- 不易扩展
对于程序员来说,上面的每一点都很致命。尤其是当嵌套层次变深之后,在一个代码块的结尾处连续出现六、七个反大括号是一件很平常的事。
这对于阅读代码的人来说简直就是一个灾难。
尤其是 switch-case
结构,不仅有上面的问题,还有如下问题:
- 很多人对
switch-case
结构的缩进有着不同的写法,虽然不影响功能但也造成阅读压力; - 容易产生
case
语句‘穿越’的情况(忘记写或者故意不写break
); - 每个
case
语句不能独享一个块级作用域,在一个case
语句中定义的变量不能在其他case
语句中重复定义; - 对于每个
case
语句,如果有相同的操作,只能重复书写。
对于前面三点来说比较容易理解,最后一条可以通过如下例子进行说明。
比如在我写的2048这个游戏中,有一段业务逻辑是这样:
switch (direction) {
case 'right':
if (canMoveRight()) {
moveRight();
}
break;
case 'left':
if (canMoveLeft()) {
moveLeft();
}
break;
case 'up':
if (canMoveUp()) {
moveUp();
}
break;
case 'down':
if (canMoveDown()) {
moveDown();
}
break;
}
复制代码
明显可以看出上面这段代码是有问题的,对于上下左右四个方向来说,执行的动作完全是一样的:判断是否可以移动,如果可以则移动。既然如此,我们就应该使用一种更清晰的结构。后面我们可以使用对象字面量进行静态配置。
复杂的条件分支语句除了带来阅读上的困难之外,还会带来不易扩展的问题。这一点很好理解,比如需要增加条件分支时,就不得不在原有的代码块上进行修改,这是不符合程序的“开放-封闭”原则的。后面我们可以使用职责链模式进行优化。
使用对象字面量进行静态配置
还是利用上面的2048游戏的上下左右的移动逻辑,如果对 Javascript 有一定理解的程序员,不难写出如下代码:
const moveByDirectionMap = {
'right': [canMoveRight, moveRight],
'left': [canMoveLeft, moveLeft],
'up': [canMoveUp, moveUp],
'down': [canMoveDown, moveDown],
};
if (moveByDirectionMap[direction][0]()) {
moveByDirectionMap[direction][1]();
}
复制代码
相比于 switch-case
结构,上面的代码易读性提高了不少,同时也避免了书写重复的操作。
在我的2048中,可以说上面这种组织代码的方式得到了充分体现。
使用职责链模式
对于分支之间没有优先级区分的情况来说,使用对象字面量进行静态配置的方法就已经足够驾驭了。然而在实际场景中,往往还会遇到各个条件分支之间是有优先判断顺序的,典型的就是 if-else
结构。
比如如下场景:
if (salary > 500) {
console.log('买耐克');
} else if (salary > 400) {
console.log('买阿迪达斯');
} else if (salary > 300) {
console.log('买李宁');
} else {
console.log('买安踏');
}
复制代码
当然现实中如果真是这么简单的场景就好了,上面这段代码用来表示存在优先级顺序的条件分支语句。接下来使用职责链模式进行优化。
首先建立 ChainNode
类:
class ChainNode {
constructor(fn) {
this.fn = fn;
this.nextFn = null;
}
pass(...args) {
const res = Reflect.apply(this.fn, this, args);
return res === 'passNext' && this.nextFn
? Reflect.apply(this.nextFn.pass, this.nextFn, args)
: res;
}
}
复制代码
然后将分支结构拆分:
const buyNike = salary => salary > 500 ? console.log('买耐克') : 'passNext';
const buyAdidas = salary => salary > 400 ? console.log('买阿迪达斯') : 'passNext';
const buyLining = salary => salary > 300 ? console.log('买李宁') : 'passNext';
const buyAnta = salary => console.log('买安踏');
复制代码
最后,构建职责链:
// 封装为职责链结点
const buyNikeNode = new ChainNode(buyNike);
const buyAdidasNode = new ChainNode(buyAdidas);
const buyLiningNode = new ChainNode(buyLining);
const buyAntaNode = new ChainNode(buyAnta);
// 制定链条顺序
buyNikeNode.nextFn = buyAdidasNode;
buyAdidasNode.nextFn = buyLiningNode;
buyLiningNode.nextFn = buyAntaNode;
// 只需调用第一个结点
buyNikeNode.pass(100);
复制代码
可以看到输出:
买安踏
复制代码
以上就是职责链模式的基本思想,可以看出,当需要新增加条件分支时,只需要插入一个职责链结点即可,相比于原来的 if-else
结构,避免了修改关键逻辑代码的危险和麻烦。需要注意的是,切不可滥用职责链模式,必须是当条件分支结构达到一定的复杂度并且需要优先级判断时,才能使用,否则只会起到反作用。
结语
写出易读易维护的代码是每个程序员的责任,这需要对程序语言本身和设计模式的的深入理解。最后列出参考资料供感兴趣的读者继续阅读。