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

动机

复杂的条件逻辑是编程中最难理解的东西之一,因此我一直在寻求给条件逻辑添加结构。很多时候,我发现可以将条件逻辑拆分到不同的场景(或者叫高阶用例),从而拆解复杂的条件逻辑。这种拆分有时用条件逻辑本身的结构就足以表达,但使用类和多态能把逻辑的拆分表述得更清晰。

一个常见的场景是:我可以构造一组类型,每个类型处理各自的一种条件逻辑。例如,我会注意到,图书、音乐、食品的处理方式不同,这是因为它们分属不同类型的商品。最明显的征兆就是有好几个函数都有基于类型代码的 switch 语句。若果真如此,我就可以针对 switch 语句中的每种分支逻辑创建一个类,用多态来承载各个类型特有的行为,从而去除重复的分支逻辑。

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

多态是面向对象编程的关键特性之一。跟其他一切有用的特性一样,它也很容易被滥用。我曾经遇到有人争论说所有条件逻辑都应该用多态取代。我不赞同这种观点。我的大部分条件逻辑只用到了基本的条件语句——if/else 和 switch/case,并不需要劳师动众地引入多态。但如果发现如前所述的复杂条件逻辑,多态是改善这种情况的有力工具。

做法

如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例。

在调用方代码中使用工厂函数获得对象实例。

将带有条件逻辑的函数移到超类中。

如果条件逻辑还未提炼至独立的函数,首先对其使用提炼函数(106)。

任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调整。

重复上述过程,处理其他条件分支。

在超类函数中保留默认情况的逻辑。或者,如果超类应该是抽象的,就把该函数声明为 abstract,或在其中直接抛出异常,表明计算责任都在子类中。

范例

我的朋友有一群鸟儿,他想知道这些鸟飞得有多快,以及它们的羽毛是什么样的。所以我们写了一小段程序来判断这些信息。

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 (bird.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 (bird.type) {
 case 'EuropeanSwallow':
  return 35;
 case 'AfricanSwallow':
  return 40 - 2 * bird.numberOfCoconuts;
 case 'NorwegianBlueParrot':
  return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;
 default:
  return null;
 }
}

有两个不同的操作,其行为都随着“鸟的类型”发生变化,因此可以创建出对应的类,用多态来处理各类型特有的行为。

我先对 airSpeedVelocity 和 plumage 两个函数使用函数组合成类(144)。

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) ? 0 : 10 + this.voltage / 10;
  default:
   return null;
  }
 }
}

然后针对每种鸟创建一个子类,用一个工厂函数来实例化合适的子类对象。

function plumage(bird) {
  return createBird(bird).plumage;
}
 
function airSpeedVelocity(bird) {
  return createBird(bird).airSpeedVelocity;
}
 
function createBird(bird) {
  switch (bird.type) {
    case "EuropeanSwallow":
      return new EuropeanSwallow(bird);
    case "AfricanSwallow":
      return new AfricanSwallow(bird);
    case "NorweigianBlueParrot":
      return new NorwegianBlueParrot(bird);
    default:
      return new Bird(bird);
  }
}
class EuropeanSwallow extends Bird {}
 
class AfricanSwallow extends Bird {}
 
class NorwegianBlueParrot extends Bird {}

现在我已经有了需要的类结构,可以处理两个条件逻辑了。先从 plumage 函数开始,我从 switch 语句中选一个分支,在适当的子类中覆写这个逻辑。

class EuropeanSwallow…

get plumage() {
  return "average";
}

class Bird…

get plumage() {
 switch (this.type) {
 case 'EuropeanSwallow':
  throw "oops";
 case 'AfricanSwallow':
  return (this.numberOfCoconuts > 2) ? "tired" : "average";
 case 'NorwegianBlueParrot':
  return (this.voltage > 100) ? "scorched" : "beautiful";
 default:
  return "unknown";
 }
}

在超类中,我把对应的逻辑分支改为抛出异常,因为我总是偏执地担心出错。

此时我就可以编译并测试。如果一切顺利的话,我可以接着处理下一个分支。

class AfricanSwallow…

get plumage() {
   return (this.numberOfCoconuts > 2) ? "tired" : "average";
}

然后是挪威蓝鹦鹉(Norwegian Blue)的分支。

class NorwegianBlueParrot…

get plumage() {
   return (this.voltage >100) ? "scorched" : "beautiful";
}

超类函数保留下来处理默认情况。

class Bird…

get plumage() {
  return "unknown";
}

airSpeedVelocity 也如法炮制。完成以后,代码大致如下(我还对顶层的 airSpeedVelocity 和 plumage 函数做了内联处理):

function plumages(birds) {
 return new Map(birds
         .map(b => createBird(b))
         .map(bird => [bird.name, bird.plumage]));
}
function speeds(birds) {
 return new Map(birds
         .map(b => createBird(b))
         .map(bird => [bird.name, 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 {
 get plumage() {
  return "average";
 }
 get airSpeedVelocity() {
  return 35;
 }
}
class AfricanSwallow extends Bird {
 get plumage() {
  return (this.numberOfCoconuts > 2) ? "tired" : "average";
 }
 get airSpeedVelocity() {
  return 40 - 2 * this.numberOfCoconuts;
 }
}
class NorwegianBlueParrot extends Bird {
 get plumage() {
  return (this.voltage > 100) ? "scorched" : "beautiful";
 }
 get airSpeedVelocity() {
  return (this.isNailed) ? 0 : 10 + this.voltage / 10;
 }
}

看着最终的代码,可以看出 Bird 超类并不是必需的。在 JavaScript 中,多态不一定需要类型层级,只要对象实现了适当的函数就行。但在这个例子中,我愿意保留这个不必要的超类,因为它能帮助阐释各个子类与问题域之间的关系。

范例:用多态处理变体逻辑

在前面的例子中,“鸟”的类型体系是一个清晰的泛化体系:超类是抽象的“鸟”,子类是各种具体的鸟。这是教科书(包括我写的书)中经常讨论的继承和多态,但并不是实践中使用继承的唯一方式。实际上,这种方式很可能不是最常用或最好的方式。另一种使用继承的情况是:我想表达某个对象与另一个对象大体类似,但又有一些不同之处。

下面有一个这样的例子:有一家评级机构,要对远洋航船的航行进行投资评级。这家评级机构会给出“A”或者“B”两种评级,取决于多种风险和盈利潜力的因素。在评估风险时,既要考虑航程本身的特征,也要考虑船长过往航行的历史。

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;
}

voyageRisk 和 captainHistoryRisk 两个函数负责打出风险分数,voyageProfitFactor 负责打出盈利潜力分数,rating 函数将 3 个分数组合到一起,给出一次航行的综合评级。

调用方的代码大概是这样:

const voyage = { zone: "west-indies", length: 10 };
const history = [
  { zone: "east-indies", profit: 5 },
  { zone: "west-indies", profit: 15 },
  { zone: "china", profit: -2 },
  { zone: "west-africa", profit: 7 },
];
 
const myRating = rating(voyage, history);

代码中有两处同样的条件逻辑,都在询问“是否有到中国的航程”以及“船长是否曾去过中国”。

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;
}

我会用继承和多态将处理“中国因素”的逻辑从基础逻辑中分离出来。如果还要引入更多的特殊逻辑,这个重构就很有用——这些重复的“中国因素”会混淆视听,让基础逻辑难以理解。

起初代码里只有一堆函数,如果要引入多态的话,我需要先建立一个类结构,因此我首先使用函数组合成类(144)。这一步重构的结果如下所示:

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.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 ExperiencedChinaRating extends Rating {}

然后,建立一个工厂函数,用于在需要时返回变体类。

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

我需要修改所有调用方代码,让它们使用该工厂函数,而不要直接调用构造函数。还好现在调用构造函数的只有 rating 函数一处。

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

有两处行为需要移入子类中。我先处理 captainHistoryRisk 中的逻辑。

class 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);
}

在子类中覆写这个函数。

class ExperiencedChinaRating

get captainHistoryRisk() {
  const result = super.captainHistoryRisk - 2;
  return Math.max(result, 0);
}

class 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);
}

分离 voyageProfitFactor 函数中的变体行为要更麻烦一些。我不能直接从超类中删掉变体行为,因为在超类中还有另一条执行路径。我又不想把整个超类中的函数复制到子类中。

class Rating…

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.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;
}

所以我先用提炼函数(106)将整个条件逻辑块提炼出来。

class Rating…

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.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;
}

函数名中出现“And”字样是一个很不好的味道,不过我会暂时容忍它,先聚焦子类化操作。

class Rating…

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

class ExperiencedChinaRating…

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

严格说来,重构到这儿就结束了——我已经把变体行为分离到了子类中,超类的逻辑理解和维护起来更简单了,只有在进入子类代码时我才需要操心变体逻辑。子类的代码表述了它与超类的差异。

但我觉得至少应该谈谈如何处理这个丑陋的新函数。引入一个函数以便子类覆写,这在处理这种“基础和变体”的继承关系时是常见操作。但这样一个难看的函数只会妨碍——而不是帮助——别人理解其中的逻辑。

函数名中的“And”字样说明其中包含了两件事,所以我觉得应该将它们分开。我会用提炼函数(106)把“历史航行数”(history length)的相关逻辑提炼出来。这一步提炼在超类和子类中都要发生,我首先从超类开始。

class Rating…

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

然后在子类中也如法炮制。

class ExperiencedChinaRating…

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

然后在超类中使用搬移语句到调用者(217)。

class Rating…

get voyageProfitFactor() {
 let result = 2;
 if (this.voyage.zone === "china") result += 1;
 if (this.voyage.zone === "east-indies") result += 1;
 result += this.historyLengthFactor;
 result += this.voyageAndHistoryLengthFactor;
 return result;
}
 
get voyageAndHistoryLengthFactor() {
 let result = 0;
 result += this.historyLengthFactor;
 if (this.voyage.length > 14) result -= 1;
 return result;
}

class ExperiencedChinaRating…

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

再用函数改名(124)改掉这个难听的名字。

class Rating…

get voyageProfitFactor() {
 let result = 2;
 if (this.voyage.zone === "china") result += 1;
 if (this.voyage.zone === "east-indies") result += 1;
 result += this.historyLengthFactor;
 result += this.voyageLengthFactor;
 return result;
}
 
get voyageLengthFactor() {
 return (this.voyage.length > 14) ? - 1: 0;
}

改为三元表达式,以简化 voyageLengthFactor 函数。

class ExperiencedChinaRating…

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

最后一件事:在“航程数”(voyage length)因素上加上 3 分,我认为这个逻辑不合理,应该把这 3 分加在最终的结果上。

class ExperiencedChinaRating…

get voyageProfitFactor() {
  return super.voyageProfitFactor + 3;
}
 
get voyageLengthFactor() {
  let result = 0;
  result += 3;
  if (this.voyage.length > 12) result += 1;
  if (this.voyage.length > 18) result -= 1;
  return result;
}

重构结束,我得到了如下代码。首先,我有一个基本的 Rating 类,其中不考虑与“中国经验”相关的复杂性:

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.historyLengthFactor;
  result += this.voyageLengthFactor;
  return result;
 }
 get voyageLengthFactor() {
  return (this.voyage.length > 14) ? - 1: 0;
 }
 get historyLengthFactor() {
  return (this.history.length > 8) ? 1 : 0;
 }
}

与“中国经验”相关的代码则清晰表述出在基本逻辑之上的一系列变体逻辑:

class ExperiencedChinaRating extends Rating {
 get captainHistoryRisk() {
  const result = super.captainHistoryRisk - 2;
  return Math.max(result, 0);
 }
 get voyageLengthFactor() {
  let result = 0;
  if (this.voyage.length > 12) result += 1;
  if (this.voyage.length > 18) result -= 1;
  return result;
 }
 get historyLengthFactor() {
  return (this.history.length > 10) ? 1 : 0;
 }
 get voyageProfitFactor() {
  return super.voyageProfitFactor + 3;
 }
}