class Department {
 get totalAnnualCost() {...}
 get name() {...}
 get headCount() {...}
}
 
class Employee {
 get annualCost() {...}
 get name() {...}
 get id() {...}
}
 
 
class Party {
 get name() {...}
 get annualCost() {...}
}
 
class Department extends Party {
 get annualCost() {...}
 get headCount() {...}
}
 
class Employee extends Party {
 get annualCost() {...}
 get id() {...}
}

动机

如果我看见两个类在做相似的事,可以利用基本的继承机制把它们的相似之处提炼到超类。我可以用字段上移(353))把相同的数据搬到超类,用函数上移(350)搬移相同的行为。

很多技术作家在谈到面向对象时,认为继承必须预先仔细计划,应该根据“真实世界”的分类结构建立对象模型。真实世界的分类结构可以作为设计继承关系的提示,但还有很多时候,合理的继承关系是在程序演化的过程中才浮现出来的:我发现了一些共同元素,希望把它们抽取到一处,于是就有了继承关系。

另一种选择就是提炼类(182)。这两种方案之间的选择,其实就是继承和委托之间的选择,总之目的都是把重复的行为收拢一处。提炼超类通常是比较简单的做法,所以我会首选这个方案。即便选错了,也总有以委托取代超类(399)这瓶后悔药可吃。

做法

为原本的类新建一个空白的超类。

如果需要的话,用改变函数声明(124)调整构造函数的签名。

测试。

使用构造函数本体上移(355)、函数上移(350)和字段上移(353))手法,逐一将子类的共同元素上移到超类。

检查留在子类中的函数,看它们是否还有共同的成分。如果有,可以先用提炼函数(106)将其提炼出来,再用函数上移(350)搬到超类。

检查所有使用原本的类的客户端代码,考虑将其调整为使用超类的接口。

范例

下面这两个类,仔细考虑之下,是有一些共同之处的——它们都有名字(name),也都有月度成本(monthly cost)和年度成本(annual cost)的概念:

class Employee {
 constructor(name, id, monthlyCost) {
  this._id = id;
  this._name = name;
  this._monthlyCost = monthlyCost;
 }
 get monthlyCost() {return this._monthlyCost;}
 get name() {return this._name;}
 get id() {return this._id;}
 
 get annualCost() {
  return this.monthlyCost * 12;
 }
}
 
class Department {
 constructor(name, staff){
  this._name = name;
  this._staff = staff;
 }
 get staff() {return this._staff.slice();}
 get name() {return this._name;}
 
 get totalMonthlyCost() {
  return this.staff
   .map(e => e.monthlyCost)
   .reduce((sum, cost) => sum + cost);
 }
 get headCount() {
  return this.staff.length;
 }
 get totalAnnualCost() {
  return this.totalMonthlyCost * 12;
 }
}

可以为它们提炼一个共同的超类,更明显地表达出它们之间的共同行为。

首先创建一个空的超类,让原来的两个类都继承这个新的类。

class Party {}
 
class Employee extends Party {
 constructor(name, id, monthlyCost) {
  super();
  this._id = id;
  this._name = name;
  this._monthlyCost = monthlyCost;
 }
 // rest of class...
class Department extends Party {
 constructor(name, staff){
  super();
  this._name = name;
  this._staff = staff;
 }
 // rest of class...

在提炼超类时,我喜欢先从数据开始搬移,在 JavaScript 中就需要修改构造函数。我先用字段上移(353))把 name 字段搬到超类中。

class Party…

constructor(name){
  this._name = name;
}

class Employee…

constructor(name, id, monthlyCost) {
  super(name);
  this._id = id;
  this._monthlyCost = monthlyCost;
}

class Department…

constructor(name, staff){
  super(name);
  this._staff = staff;
}

把数据搬到超类的同时,可以用函数上移(350)把相关的函数也一起搬移。首先是 name 函数:

class Party…

get name() {return this._name;}

class Employee…

get name() {return this._name;}

class Department…

get name() {return this._name;}

有两个函数实现非常相似。

class Employee…

get annualCost() {
  return this.monthlyCost * 12;
}

class Department…

get totalAnnualCost() {
  return this.totalMonthlyCost * 12;
}

它们各自使用的函数 monthlyCost 和 totalMonthlyCost 名字和实现都不同,但意图却是一致。我可以用改变函数声明(124)将它们的名字统一。

class Department…

get totalAnnualCost() {
  return this.monthlyCost * 12;
}
 
get monthlyCost() { ... }

然后对计算年度成本的函数也做相似的改名:

class Department…

get annualCost() {
  return this.monthlyCost * 12;
}

现在可以用函数上移(350)把这个函数搬到超类了。

class Party…

get annualCost() {
  return this.monthlyCost * 12;
}

class Employee…

get annualCost() {
  return this.monthlyCost * 12;
}

class Department…

get annualCost() {
  return this.monthlyCost * 12;
}