Skip to content

Latest commit

 

History

History
675 lines (547 loc) · 18.7 KB

第十章:简化条件逻辑.md

File metadata and controls

675 lines (547 loc) · 18.7 KB

简化条件逻辑

程序的复杂度大多来自条件逻辑。因此可以借助重构把条件逻辑变得更容易理解。

  • 使用分解条件表达式处理复杂的条件表达式
  • 合并条件表达式理清逻辑组合
  • 以卫语句取代嵌套条件表达式表达"在主要处理逻辑之前先做检查"的意图
  • 以多态取代条件表达式可以处理switch逻辑

很多条件逻辑是用于处理特殊情况的,如处理null值。

  • 可以用引入特例处理逻辑大多相同的情况,消除重复代码

分解条件表达式(Decompose Conditional)

合并条件表达式(Consolidate Conditional Expression)

以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)

以多态取代条件表达式(Replace Conditional with Polymorphism)

引入特例(Introduce Special Case)

分解条件表达式

程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。大型函数本身就会使代码的可读性下降,而条件逻辑则会使代码更难阅读。

对于复杂逻辑的函数,和任何大块头代码一样,可以将它分解为多个独立的函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函数,从而更清楚地表达自己的意图。对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。

例子

// 重构前
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
  charge = quantity * plan.summerRate;
else
  charge = quantity * plan.regularRate + plan.regularServiceCharge;

对上面的例子,可以将条件抽成一个函数,判断为真的分支抽成一个函数,判断为假的分支抽成一个函数。

if (summer())
  charge = summerCharge();
else
  charge = regularCharge();

function summer() {
	return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}

function summerCharge() {
  return quantity * plan.summerRate;
}

function regularCharge() {
  return quantity * plan.regularRate + plan.regularServiceCharge;
}

最后,可以将原来的if语句改成三元运算符安排条件语句

// 重构后
charge = summer() ? summerCharge() : regularCharge();

function summer() {
	return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}

function summerCharge() {
  return quantity * plan.summerRate;
}

function regularCharge() {
  return quantity * plan.regularRate + plan.regularServiceCharge;
}

合并条件表达式

如果一串检查条件各不相同,但最终行为一致,就应该使用"逻辑或"和"逻辑与"将它们合并为一个条件表达式。

例子

// 重构前
function disabilityAmount(anEmployee) {
  if (anEmployee.seniority < 2) return 0;
  if (anEmployee.monthsDisabled > 12) return 0;
  if (anEmployee.isPartTime) return 0;
  // compute the disability amount
}

例子中三个条件最终的返回都是0,可以直接用逻辑或运算符来合并

function disabilityAmount(anEmployee) {
  if ((anEmployee.seniority < 2)
     || (anEmployee.monthsDisabled > 12)
     || (anEmployee.isPartTime))
    return 0;
  // compute the disability amount
}

最后,对这个条件表达式使用提炼函数

// 重构后
function disabilityAmount(anEmployee) {
  if (isNotEligableForDisability()) return 0;
  // compute the disability amount
}

function isNotEligableForDisability() {
  return ((anEmployee.seniority < 2)
     || (anEmployee.monthsDisabled > 12)
     || (anEmployee.isPartTime));
}

以卫语句取代嵌套条件表达式

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

如果两条分支都是正常行为,就应该使用形如if … else …的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时从函数中返回。这样的单独检查常常被称为"卫语句"(guard clauses)。

卫语句的精髓就是:给某一条分支以特别的重视。如果使用if … else …结构,对if分支和else分支的重视都是同等的。卫语句不同,告诉读者:"这种情况不是本函数的核心逻辑所关心的,如果真的发生了,请做一些必要的整理工作,然后退出"。

例1

// 重构前
function payAmount(employee) {
  let result;
  if (employee.isSeparated) {
    result = { amount: 0, reasonable: 'SEP' };
  } else {
    if (employee.isRetired) {
      result = { amount: 0, reasonable: 'RET' };
    } else {
      // logic to compute amount
      result = someFinalComputation();
    }
  }
  return result;
}

上面嵌套的条件逻辑看不清代码真实的含义。只有当前两个条件表达式都不为真的时候,这段代码才真正开始它的主要工作。

使用卫语句,去除else分支,并去掉局部变量result

// 重构后
function payAmount(employee) {
  if (employee.isSeparated) return { amount: 0, reasonable: 'SEP' };
  if (employee.isRetired) return { amount: 0, reasonable: 'RET' };
  return someFinalComputation();
}

以多态取代条件表达式

如果有好几个函数都有基于类型代码的switch语句,完全可以针对这种分支逻辑创建一个类,用多态来承载各个类型特有的行为,从而去除重复的分支逻辑。

基础逻辑可能是最常用的,也可能是最简单的。可以把基础逻辑放进超类,这样就首先可以理解在和部分逻辑,暂时不管各种变体,然后可以把每种变体逻辑单独放进一个子类,其中的代码着重强调与基础逻辑的差异。

例子

// 重构前
function plumages(birds) {
  return new Map(birds.map(b => [b.name, plumage(b)]));
}

function speeds(birds) {
  return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}

function plumage(bird) {
  switch (brid.type) {
    case 'EuropeanSwallow':
      return 'average';
    case 'AfricanSwallow':
      return (bird.numberOfCoconuts > 2) ? 'tired' : 'average';
    case 'NorwegianBlueParrot':
      return (bird.voltage > 100) ? 'scorched' : 'beautiful';
    default:
      return 'unknown';
  }
}

function airSpeedVelocity(bird) {
  switch (brid.type) {
    case 'EuropeanSwallow':
      return 35;
    case 'AfricanSwallow':
      return 40 - 2 * bird.numberOfCoconuts;
    case 'NorwegianBlueParrot':
      return (bird.isNailed > 100) ? 0 : 10 + bird.voltage / 10;
    default:
      return null;
  }
}

以上示例可以用多态处理各类型特有的行为进行重构

先将airSpeedVelocity和plumage两个函数使用函数组合成类

function plumage(bird) {
  return new Bird(bird).plumage;
}

function airSpeedVelocity(bird) {
  return new Bird(bird).airSpeedVelocity;
}

class Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }
  
  get plumage() {
    switch (this.type) {
      case 'EuropeanSwallow':
        return 'average';
      case 'AfricanSwallow':
        return (this.numberOfCoconuts > 2) ? 'tired' : 'average';
      case 'NorwegianBlueParrot':
        return (this.voltage > 100) ? 'scorched' : 'beautiful';
      default:
        return 'unknown';
    }
  }
  
  get airSpeedVelocity() {
    switch (this.type) {
      case 'EuropeanSwallow':
        return 35;
      case 'AfricanSwallow':
        return 40 - 2 * this.numberOfCoconuts;
      case 'NorwegianBlueParrot':
        return (this.isNailed > 100) ? 0 : 10 + this.voltage / 10;
      default:
        return null;
    }
  }
}

接着对每种鸟创建一个子类,用一个工厂函数来实例化合适的子类对象:

function plumage(bird) {
  return new Bird(bird).plumage;
}

function airSpeedVelocity(bird) {
  return new Bird(bird).airSpeedVelocity;
}

function createBird(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return new EuropeanSwallow(bird);
    case 'AfricanSwallow':
      return new AfricanSwallow(bird);
    case 'NorwegianBlueParrot':
      return new NorwegianBlueParrot(bird);
    default:
      return new Bird(bird);
  }
}

class Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }
  
  get plumage() {
    switch (this.type) {
      case 'EuropeanSwallow':
        return 'average';
      case 'AfricanSwallow':
        return (this.numberOfCoconuts > 2) ? 'tired' : 'average';
      case 'NorwegianBlueParrot':
        return (this.voltage > 100) ? 'scorched' : 'beautiful';
      default:
        return 'unknown';
    }
  }
  
  get airSpeedVelocity() {
    switch (this.type) {
      case 'EuropeanSwallow':
        return 35;
      case 'AfricanSwallow':
        return 40 - 2 * this.numberOfCoconuts;
      case 'NorwegianBlueParrot':
        return (this.isNailed > 100) ? 0 : 10 + this.voltage / 10;
      default:
        return null;
    }
  }
}

class EuropeanSwallow extends Bird {}

class AfricanSwallow extends Bird {}

class NorwegianBlueParrot extends Bird {}

接着将Bird类中的airSpeedVelocity、plumage的实现移入到子类中,父类中只留下默认的返回

// 重构后
function plumage(bird) {
  return new Bird(bird).plumage;
}

function airSpeedVelocity(bird) {
  return new Bird(bird).airSpeedVelocity;
}

function createBird(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return new EuropeanSwallow(bird);
    case 'AfricanSwallow':
      return new AfricanSwallow(bird);
    case 'NorwegianBlueParrot':
      return new NorwegianBlueParrot(bird);
    default:
      return new Bird(bird);
  }
}

class Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }
  
  get plumage() {
    return 'unknown';
  }
  
  get airSpeedVelocity() {
    return null;
  }
}

class EuropeanSwallow extends Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }
  
  get plumage() {
    return 'average';
  }
  
  get airSpeedVelocity() {
    return 35;
  }
}

class AfricanSwallow extends Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }
  
  get plumage() {
    return (this.numberOfCoconuts > 2) ? 'tired' : 'average';
  }
  
  get airSpeedVelocity() {
    return 40 - 2 * this.numberOfCoconuts;
  }
}

class NorwegianBlueParrot extends Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }
  
  get plumage() {
    return (this.voltage > 100) ? 'scorched' : 'beautiful';
  }
  
  get airSpeedVelocity() {
    return (this.isNailed > 100) ? 0 : 10 + this.voltage / 10;
  }
}

例2 用多态处理变体逻辑

存在一种继承使用情况:某个对象与另一个对象大体类似,但又有一些不同之处。

// 重构前
function rating(voyage, history) {
  const vpf = voyageProfitFactor(voyage, history);
  const vr = voyageRisk(voyage);
  const chr = captainHistoryRisk(voyage, history);
  if (vpf * 3 > (vr + chr * 2)) return 'A';
  else return 'B';
}

function voyageRisk(voyage) {
  let result = 1;
  if (voyage.length > 4) result += 2;
  if (voyage.length > 8) result += voyage.length - 8;
  if (['china', 'east-indies'].includes(voyage.zone)) result += 4;
  return Math.max(result, 0);
}

function captainHistoryRisk(voyage, history) {
  let result = 1;
  if (history.length < 5) result += 4;
  result += history.filter(v => v.profit < 0).length;
  if (voyage.zone === 'china' && hasChina(history)) result -= 2;
  return Math.max(result, 0);
}

function hasChina(history) {
  return history.some(v => 'china' === v.zone);
}

function voyageProfitFactor(voyage, history) {
  let result = 2;
  if (voyage.zone === 'china') result += 1;
  if (voyage.zone === 'east-indies') result += 1;
  if (voyage.zone === 'china' && hasChina(history)) {
    result += 3;
    if (history.length > 10) result += 1;
    if (voyage.length > 12) result += 1;
    if (voyage.length > 18) result -= 1;
  } else {
    if (history.length > 8) result += 1;
    if (voyage.length > 14) result -= 1;
  }
  return result;
}

注意到在captainHistoryRisk、voyageProfitFactor两个函数中voyage.zone === 'china' && hasChina(history)出现了两次。可以将这个逻辑从基础逻辑中抽离出来当做子类继承。

首先,需要引入多态,建立类结构,使用函数组合成类

function rating(voyage, history) {
  return new Rating(voyage, history).value;
}

class Rating {
  constructor(voyage, history) {
    this.voyage = voyage;
    this.history = history;
  }
  
  get value() {
    const vpf = this.voyageProfitFactor;
    const vr = this.voyageRisk;
    const chr = this.captainHistoryRisk;
    if (vpf * 3 > (vr + chr * 2)) return 'A';
    else return 'B';
  }
  
  get voyageRisk() {
    let result = 1;
    if (this.voyage.length > 4) result += 2;
    if (this.voyage.length > 8) result += this.voyage.length - 8;
    if (['china', 'east-indies'].includes(this.voyage.zone)) result += 4;
    return Math.max(result, 0);
  }

  get captainHistoryRisk() {
    let result = 1;
    if (this.history.length < 5) result += 4;
    result += this.history.filter(v => v.profit < 0).length;
    if (this.voyage.zone === 'china' && this.hasChinaHistory) result -= 2;
    return Math.max(result, 0);
  }

  get voyageProfitFactor() {
    let result = 2;
    if (this.voyage.zone === 'china') result += 1;
    if (this.voyage.zone === 'east-indies') result += 1;
    if (this.voyage.zone === 'china' && this.hasChinaHistory) {
      result += 3;
      if (this.this.history.length > 10) result += 1;
      if (this.voyage.length > 12) result += 1;
      if (this.voyage.length > 18) result -= 1;
    } else {
      if (this.history.length > 8) result += 1;
      if (this.voyage.length > 14) result -= 1;
    }
    return result;
  }
  
  get hasChinaHistory() {
    return this.history.some(v => 'china' === v.zone);
  }
}

接着,创建一个空的子类,用来存储与超类不同的行为;然后,建立工厂函数,替换原来直接调用构造函数的方式

class ExperienceChinaRating extends Rating {}

function createRating(voyage, history) {
  if (voyage.zone === 'china' && history.some(v => 'china' === v.zone)) {
    return new ExperienceChinaRating(voyage, history);
  }
  return new Rating(voyage, history);
}

function rating(voyage, history) {
  return createRating(voyage, history).value;
}

移植超类中需要移入子类的逻辑,需要注意voyageProfitFactor函数,可以先用提炼函数将整个条件逻辑块提炼出来

get voyageProfitFactor() {
  let result = 2;
  if (this.voyage.zone === 'china') result += 1;
  if (this.voyage.zone === 'east-indies') result += 1;
  result += this.voyageAndHistoryLengthFactor;
  return result;
}

get voyageAndHistoryLengthFactor() {
  let result = 0;
  if (this.voyage.zone === 'china' && this.hasChinaHistory) {
    result += 3;
    if (this.this.history.length > 10) result += 1;
    if (this.voyage.length > 12) result += 1;
    if (this.voyage.length > 18) result -= 1;
  } else {
    if (this.history.length > 8) result += 1;
    if (this.voyage.length > 14) result -= 1;
  }
  return result;
}

接下来,可以拆解voyageAndHistoryLengthFactor使其子类化

class Rating {
  get voyageAndHistoryLengthFactor() {
    let result = 0;
    if (this.history.length > 8) result += 1;
    if (this.voyage.length > 14) result -= 1;
    return result;
  }
}

class ExperienceChinaRating {
  get voyageAndHistoryLengthFactor() {
    let result = 0;
    result += 3;
    if (this.this.history.length > 10) result += 1;
    if (this.voyage.length > 12) result += 1;
    if (this.voyage.length > 18) result -= 1;
    return result;
  }
}

最后,整合所有修改

// 重构后
function createRating(voyage, history) {
  if (voyage.zone === 'china' && history.some(v => 'china' === v.zone)) {
    return new ExperienceChinaRating(voyage, history);
  }
  return new Rating(voyage, history);
}

function rating(voyage, history) {
  return createRating(voyage, history).value;
}

class Rating {
  constructor(voyage, history) {
    this.voyage = voyage;
    this.history = history;
  }
  
  get value() {
    const vpf = this.voyageProfitFactor;
    const vr = this.voyageRisk;
    const chr = this.captainHistoryRisk;
    if (vpf * 3 > (vr + chr * 2)) return 'A';
    else return 'B';
  }
  
  get voyageRisk() {
    let result = 1;
    if (this.voyage.length > 4) result += 2;
    if (this.voyage.length > 8) result += this.voyage.length - 8;
    if (['china', 'east-indies'].includes(this.voyage.zone)) result += 4;
    return Math.max(result, 0);
  }

  get captainHistoryRisk() {
    let result = 1;
    if (this.history.length < 5) result += 4;
    result += this.history.filter(v => v.profit < 0).length;
    return Math.max(result, 0);
  }

  get voyageProfitFactor() {
    let result = 2;
    if (this.voyage.zone === 'china') result += 1;
    if (this.voyage.zone === 'east-indies') result += 1;
    result += this.voyageAndHistoryLengthFactor;
    return result;
  }
  
  get voyageAndHistoryLengthFactor() {
    let result = 0;
    if (this.history.length > 8) result += 1;
    if (this.voyage.length > 14) result -= 1;
    return result;
  }
  
  get hasChinaHistory() {
    return this.history.some(v => 'china' === v.zone);
  }
}

class ExperienceChinaRating extends Rating {
  get captainHistoryRisk() {
    let result = 1;
    if (this.history.length < 5) result += 4;
    result += this.history.filter(v => v.profit < 0).length;
    if (this.voyage.zone === 'china' && this.hasChinaHistory) result -= 2;
    return Math.max(result, 0);
  }

  get voyageAndHistoryLengthFactor() {
    let result = 0;
    result += 3;
    if (this.this.history.length > 10) result += 1;
    if (this.voyage.length > 12) result += 1;
    if (this.voyage.length > 18) result -= 1;
    return result;
  }
}

在作者书中,接下来还是会继续重构简化函数,但已经脱离了对这个重构方法的理解,不再继续下去。

引入特例

有一种常见的重复代码的情况:一个数据结构的使用者都在检查某个特殊的值,并且当这个特殊值出现时所做的处理也都相同。如果发现代码库中有多处以同样方式应对同一个特殊值,应该把这个处理逻辑收拢在一起。

可以创建一个特例元素,用以表达对这种特例的公用行为的处理。

如果只需要从这个对象读取数据,可以提供一个字面量对象(literal object),其中所有的值都是预先填充好的。如果除简单的数值之外还需要更多的行为,就需要创建一个特殊对象,其中包含所有公用行为所对应的函数。

一个通常需要特殊处理的值就是null,这个模式常被叫做"Null对象"(Null Object)模式。