function getPayAmount() {
  let result;
  if (isDead) result = deadAmount();
  else {
    if (isSeparated) result = separatedAmount();
    else {
      if (isRetired) result = retiredAmount();
      else result = normalPayAmount();
    }
  }
  return result;
}
 
function getPayAmount() {
  if (isDead) return deadAmount();
  if (isSeparated) return separatedAmount();
  if (isRetired) return retiredAmount();
  return normalPayAmount();
}

动机

根据我的经验,条件表达式通常有两种风格。第一种风格是:两个条件分支都属于正常行为。第二种风格则是:只有一个条件分支是正常行为,另一个分支则是异常的情况。

这两类条件表达式有不同的用途,这一点应该通过代码表现出来。如果两条分支都是正常行为,就应该使用形如 if…else…的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”(guard clauses)。

以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。如果使用 if-then-else 结构,你对 if 分支和 else 分支的重视是同等的。这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它告诉阅读者:“这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请做一些必要的整理工作,然后退出。”

“每个函数只能有一个入口和一个出口”的观念,根深蒂固于某些程序员的脑海里。我发现,当我处理他们编写的代码时,经常需要使用以卫语句取代嵌套条件表达式。现今的编程语言都会强制保证每个函数只有一个入口,至于“单一出口”规则,其实不是那么有用。在我看来,保持代码清晰才是最关键的:如果单一出口能使这个函数更清楚易读,那么就使用单一出口;否则就不必这么做。

做法

选中最外层需要被替换的条件逻辑,将其替换为卫语句。

测试。

有需要的话,重复上述步骤。

如果所有卫语句都引发同样的结果,可以使用合并条件表达式(263)合并之。

范例

下面的代码用于计算要支付给员工(employee)的工资。只有还在公司上班的员工才需要支付工资,所以这个函数需要检查两种“员工已经不在公司上班”的情况。

function payAmount(employee) {
 let result;
 if(employee.isSeparated) {
  result = {amount: 0, reasonCode:"SEP"};
 }
 else {
  if (employee.isRetired) {
   result = {amount: 0, reasonCode: "RET"};
  }
  else {
   // logic to compute amount
   lorem.ipsum(dolor.sitAmet);[^1]
   consectetur(adipiscing).elit();
   sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
   ut.enim.ad(minim.veniam);
   result = someFinalComputation();
  }
 }
 return result;
}

嵌套的条件逻辑让我们看不清代码真实的含义。只有当前两个条件表达式都不为真的时候,这段代码才真正开始它的主要工作。所以,卫语句能让代码更清晰地阐述自己的意图。

一如既往地,我喜欢小步前进,所以我先处理最顶上的条件逻辑。

function payAmount(employee) {
 let result;
 if (employee.isSeparated) return {amount: 0, reasonCode: "SEP"};
 if (employee.isRetired) {
  result = {amount: 0, reasonCode: "RET"};
 }
 else {
  // logic to compute amount
  lorem.ipsum(dolor.sitAmet);
  consectetur(adipiscing).elit();
  sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
  ut.enim.ad(minim.veniam);
  result = someFinalComputation();
 }
 return result;
}

做完这步修改,我执行测试,然后继续下一步。

function payAmount(employee) {
 let result;
 if (employee.isSeparated) return {amount: 0, reasonCode: "SEP"};
 if (employee.isRetired)   return {amount: 0, reasonCode: "RET"};
 // logic to compute amount
 lorem.ipsum(dolor.sitAmet);
 consectetur(adipiscing).elit();
 sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
 ut.enim.ad(minim.veniam);
 result = someFinalComputation();
 return result;
}

此时,result 变量已经没有用处了,所以我把它删掉:

function payAmount(employee) {
 let result;
 if (employee.isSeparated) return {amount: 0, reasonCode: "SEP"};
 if (employee.isRetired)   return {amount: 0, reasonCode: "RET"};
 // logic to compute amount
 lorem.ipsum(dolor.sitAmet);
 consectetur(adipiscing).elit();
 sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
 ut.enim.ad(minim.veniam);
 return someFinalComputation();
}

能减少一个可变变量总是好的。

范例:将条件反转

审阅本书第 1 版的初稿时,Joshua Kerievsky 指出:我们常常可以将条件表达式反转,从而实现以卫语句取代嵌套条件表达式。为了拯救我可怜的想象力,他还好心帮我想了一个例子:

function adjustedCapital(anInstrument) {
 let result = 0;
 if (anInstrument.capital > 0) {
  if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
   result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
  }
 }
 return result;
}

同样地,我逐一进行替换。不过这次在插入卫语句时,我需要将相应的条件反转过来:

function adjustedCapital(anInstrument) {
 let result = 0;
 if (anInstrument.capital <= 0) return result;
 if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
  result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
 }
 return result;
}

下一个条件稍微复杂一点,所以我分两步进行反转。首先加入一个逻辑非操作:

function adjustedCapital(anInstrument) {
 let result = 0;
 if (anInstrument.capital <= 0) return result;
 if (!(anInstrument.interestRate > 0 && anInstrument.duration > 0)) return result;
 result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
 return result;
}

但是在这样的条件表达式中留下一个逻辑非,会把我的脑袋拧成一团乱麻,所以我把它简化成下面这样:

  function adjustedCapital(anInstrument) {
 let result = 0;
 if (anInstrument.capital <= 0) return result;
 if (anInstrument.interestRate <= 0 || anInstrument.duration <= 0) return result;
 result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
 return result;
}

这两行逻辑语句引发的结果一样,所以我可以用合并条件表达式(263)将其合并。

function adjustedCapital(anInstrument) {
 let result = 0;
 if (   anInstrument.capital      <= 0
   || anInstrument.interestRate <= 0
   || anInstrument.duration     <= 0) return result;
 result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
 return result;
}

此时 result 变量做了两件事:一开始我把它设为 0,代表卫语句被触发时的返回值;然后又用最终计算的结果给它赋值。我可以彻底移除这个变量,避免用一个变量承担两重责任,而且又减少了一个可变变量。

function adjustedCapital(anInstrument) {
 if (   anInstrument.capital     <= 0
   || anInstrument.interestRate <= 0
   || anInstrument.duration   <= 0) return 0;
 return (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
}