阅读 303

如何编写易读的条件分支结构

我不喜欢写 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 结构,避免了修改关键逻辑代码的危险和麻烦。需要注意的是,切不可滥用职责链模式,必须是当条件分支结构达到一定的复杂度并且需要优先级判断时,才能使用,否则只会起到反作用。


结语

写出易读易维护的代码是每个程序员的责任,这需要对程序语言本身和设计模式的的深入理解。最后列出参考资料供感兴趣的读者继续阅读。

参考资料

关注下面的标签,发现更多相似文章
评论