函数上移

动机:如果某个函数在各个子类中的函数体都相同,则将函数上移

  1. 检查待上移的函数,确定完全一致
  2. 检查函数体内引用的所有函数调用和字段都能从超类中调用到
  3. 如果待上移的函数声明不同,则修改为将要在超类中使用的声明
  4. 超类中创建一个函数,将待上移函数代码复制其中
  5. 执行静态检查
  6. 移除一个待上移子类函数
  7. 测试
  8. 移除其余待上移子类函数

Before:

1
2
3
4
5
6
7
8
9
10
class Employee extends Party {
get annualCost() {
return this.monthlyCost * 12;
}
}
class Department extends Party {
get annualCost() {
return this.monthlyCost * 12;
}
}

After:

1
2
3
4
5
6
7
8
9
class Party {
get annualCost() {
return this.monthlyCost * 12;
}

get monthlyCost() {
throw new SubclassResponsibilityError();
}
}

字段上移

动机:观察函数如何使用字段来判断它们是否重复,如果它们被使用方式很相似,则可以将它们上移到超类中去

  1. 检查待上移的字段使用方式一致
  2. 如果在在类中名字不一致,则取相同的名字
  3. 超类中创建一个字段
  4. 移除子类字段
  5. 测试

Before:

1
2
3
4
5
6
7
class Employee {}
class Salesman extends Employee {
name;
}
class Engineer extends Employee {
name;
}

After:

1
2
3
4
5
class Employee {
name;
}
class Salesman extends Employee {}
class Engineer extends Employee {}

构造函数本体上移

动机: 各个子类中构造函数有共同的行为

  1. 超类不存在构造函数,则创建一个,并确保子类调用
  2. 将子类构造函数中的公共语句移动到超类构造函数中
  3. 删除子类构造函数公共代码
  4. 测试
  5. 如存在无法简单上移至超类的公共代码,利用函数上移提升

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Party {}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super();
this._id = id;
this._name = name;
this._monthlyCost = monthlyCost;
}
}
class Department extends Party {
constructor(name, staff) {
super();
this._name = name;
this._staff = staff;
}
}

After:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Party {
constructor(name) {
this._name = name;
}
}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super(name);
this._id = id;
this._monthlyCost = monthlyCost;
}
}
class Department extends Party {
constructor(name, staff) {
super(name);
this._staff = staff;
}
}

函数下移

动机:如果超类中的某个函数只与一个子类有关,最好将其从超类中移除,放到正在关心它的子类中去

  1. 将超类中的函数本体复制到需要此函数的子类中
  2. 删除超类中的函数
  3. 测试
  4. 将该函数从所有不需要的子类中删除
  5. 测试

字段下移

动机:如果属性字段只被一个子类用到,则下移至该子类中

  1. 在子类中声明该字段
  2. 从超类中移除
  3. 测试
  4. 将该字段从不需要它的子类中删除
  5. 测试

以子类取代类型码

动机:继承可以用多态来处理条件逻辑,更能明确地表达数据和类型之间的关系

  1. 自封装类型码字段
  2. 任选一个类型码取值,为其创建一个子类
  3. 创建一个选择器逻辑,把类型码参数映射到新的子类
  4. 测试
  5. 针对每个类型码取值,重复 2
  6. 去除类型码字段
  7. 测试
  8. 处理原本访问类型码的函数

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Employee {
constructor(name, type) {
this.validateType(type);
this._name = name;
this._type = type;
}
validateType(arg) {
if (!["engineer", "manager", "salesman"].includes(arg))
throw new Error(`Employee cannot be of type ${arg}`);
}
toString() {
return `${this._name} (${this._type})`;
}
}

After:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Employee {
constructor(name, type) {
this._name = name;
}
}
class Engineer extends Employee {
get type() {
return "engineer";
}
}
class Salesman extends Employee {
get type() {
return "salesman";
}
}
class Manager extends Employee {
get type() {
return "manager";
}
}
function createEmployee(name, type) {
switch (type) {
case "engineer":
return new Engineer(name);
case "salesman":
return new Salesman(name);
case "manager":
return new Manager(name);
default:
throw new Error(`Employee cannot be of type ${type}`);
}
}

移除子类

动机:如果子类的用处太少,最好移除子类,将其替换为超类的一个字段

  1. 把子类的构造函数包装到超类的工厂函数中
  2. 将类型检查逻辑包装起来搬移到超类
  3. 新建一个字段,代表子类类型
  4. 将判断子类类型的函数改为新建字段
  5. 删除子类
  6. 测试

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
get genderCode() {
return "X";
}
}
class Male extends Person {
get genderCode() {
return "M";
}
}
class Female extends Person {
get genderCode() {
return "F";
}
}

After:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createPerson(aRecord) {
switch (aRecord.gender) {
case "M":
return new Person(aRecord.name, "M");
case "F":
return new Person(aRecord.name, "F");
default:
return new Person(aRecord.name);
}
}
class Person {
constructor(name, genderCode) {
this._name = name;
this._genderCode = genderCode;
}

get genderCode() {
return this._genderCode;
}
}

提炼超类

动机:如果两个类在做相似的事,可以利用继承把相似之处提炼到超类。

  1. 为原本类新建一个空的超类
  2. 测试
  3. 逐一将子类共同元素上移至超类
  4. 检查子类中的函数,看是否还有共同的成分,有则提炼并上移
  5. 检查所有原本的类,将其调整为使用超类接口

折叠继承体系

动机:如果一个子类和超类已经没多大差别,则将子类和超类合并起来

  1. 选择移除超类还是子类?
  2. 将所有元素移动到同一个类中
  3. 修改将被移除类的所有引用点,改为合并后留下的类
  4. 移除类
  5. 测试

以委托取代子类

动机:与继承相比使用委托关系时接口更清晰、耦合更少,对象组合常常优于类继承

  1. 如果构造函数有多个调用者,首先工厂函数把构造函数包装起来
  2. 创建一个空的委托类
  3. 在超类中添加一个字段,用于安放委托对象
  4. 修改子类创建逻辑,使其初始化委托字段,放入一个委托对象的实例中
  5. 选择一个子类的函数,将其移入委托类
  6. 搬移上述函数,不要删除类中的委托代码
  7. 如果原函数在子类之外被调用,则把委托代码上移至超类,如果子类外没有调用,则移除委托代码
  8. 测试
  9. 重复,直到子类中所有函数都搬到委托类
  10. 找到所有子类构造函数的地方,逐一改为使用超类的构造函数
  11. 测试
  12. 移除子类

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Booking {
constructor(show, date) {
this._show = show;
this._date = date;
}
get hasTalkback() {
return this._show.hasOwnProperty("talkback") && !this.isPeakDay;
}
get basePrice() {
let result = this._show.price;
if (this.isPeakDay) result += Math.round(result * 0.15);
return result;
}
}

class PremiumBooking extends Booking {
constructor(show, date, extras) {
super(show, date);
this._extras = extras;
}
get hasTalkback() {
return this._show.hasOwnProperty("talkback");
}
get basePrice() {
return Math.round(super.basePrice + this._extras.premiumFee);
}
get hasDinner() {
return this._extras.hasOwnProperty("dinner") && !this.isPeakDay;
}
}

After:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Booking {
constructor(show, date) {
this._show = show;
this._date = date;
}
_bePremium(extras) {
this._premiumDelegate = new PremiumBookingDelegate(this, extras);
}

get hasTalkback() {
return this._premiumDelegate
? this._premiumDelegate.hasTalkback
: this._show.hasOwnProperty("talkback") && !this.isPeakDay;
}
get basePrice() {
let result = this._show.price;
if (this.isPeakDay) result += Math.round(result * 0.15);
return this._premiumDelegate
? this._premiumDelegate.extendBasePrice(result)
: result;
}
get hasDinner() {
return this._premiumDelegate ? this._premiumDelegate.hasDinner : undefined;
}
}

function createBooking(show, date) {
return new Booking(show, date);
}
function createPremiumBooking(show, date, extras) {
const result = new Booking(show, date);
result._bePremium(extras);
return result;
}

class PremiumBookingDelegate {
constructor(hostBooking, extras) {
this._host = hostBooking;
this._extras = extras;
}
get hasTalkback() {
return this._host._show.hasOwnProperty("talkback");
}
extendBasePrice(base) {
return Math.round(base + this._extras.premiumFee);
}
get hasDinner() {
return this._extras.hasOwnProperty("dinner") && !this._host.isPeakDay;
}
}

以委托取代超类

动机:如果超类的一些函数对子类并不适用,则以委托取代超类

  1. 在子类中新建一个字段,使其引用超类的一个对象,并将委托引用初始化为超类的新实例
  2. 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发委托引用
  3. 当所有超类函数都被转发函数覆写后,去掉继承关系

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CatalogItem {
constructor(id, title, tags) {
this._id = id;
this._title = title;
this._tags = tags;
}
get id() {
return this._id;
}
get title() {
return this._title;
}
hasTag(arg) {
return this._tags.includes(arg);
}
}

class Scroll extends CatalogItem {
constructor(id, title, tags, dateLastCleaned) {
super(id, title, tags);
this._lastCleaned = dateLastCleaned;
}
needsCleaning(targetDate) {
const threshold = this.hasTag("revered") ? 700 : 1500;
return this.daysSinceLastCleaning(targetDate) > threshold;
}
daysSinceLastCleaning(targetDate) {
return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
}
}

After:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class CatalogItem {
constructor(id, title, tags) {
this._id = id;
this._title = title;
this._tags = tags;
}
get id() {
return this._id;
}
get title() {
return this._title;
}
hasTag(arg) {
return this._tags.includes(arg);
}
}

class Scroll {
constructor(id, dateLastCleaned, catalogID, catalog) {
this._id = id;
this._catalogItem = catalog.get(catalogID);
this._lastCleaned = dateLastCleaned;
}
get id() {
return this._id;
}
get title() {
return this._catalogItem.title;
}
hasTag(aString) {
return this._catalogItem.hasTag(aString);
}
needsCleaning(targetDate) {
const threshold = this.hasTag("revered") ? 700 : 1500;
return this.daysSinceLastCleaning(targetDate) > threshold;
}
daysSinceLastCleaning(targetDate) {
return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
}
}