(八)掌握继承的艺术:重构之路,化繁为简
代码重构专题文章
(一)代码匠心:重构之道,化腐朽为神奇
(二)重构的艺术:精进代码的第一组基本功
(三)封装与结构优化:让代码更优雅
(四)优雅重构:洞悉“搬移特性”的艺术与实践
(五)数据重构的艺术:优化你的代码结构与可读性
(六)重构的艺术:简化复杂条件逻辑的秘诀
(七)API 重构的艺术:打造优雅、可维护的 API
(八)掌握继承的艺术:重构之路,化繁为简
文章目录
- 代码重构专题文章
- 第 12 章 掌握继承的艺术:重构之路,化繁为简
- 12.0 前言
- 12.1 函数上移(Pull Up Method)
- 动机
- 做法
- 范例
- 12.2 字段上移(Pull Up Field)
- 动机
- 做法
- 范例
- 12.3 构造函数本体上移(Pull Up Constructor Body)
- 动机
- 做法
- 范例
- 12.4 函数下移(Push Down Method)
- 动机
- 做法
- 范例
- 12.5 字段下移(Push Down Field)
- 动机
- 做法
- 范例
- 12.6 以子类取代类型码(Replace Type Code with Subclasses)
- 动机
- 做法
- 范例
- 范例:直接继承
- 范例:间接继承 (以对象取代基本类型 + 以子类取代类型码)
- 12.7 移除子类(Remove Subclass)
- 动机
- 做法
- 范例
- 12.8 提炼超类(Extract Superclass)
- 动机
- 做法
- 范例
- 12.9 折叠继承体系(Collapse Hierarchy)
- 动机
- 做法
- 范例
- 12.10 以委托取代子类(Replace Subclass with Delegate)
- 动机
- 做法
- 范例
- 范例:取代继承体系
- 12.11 以委托取代超类(Replace Superclass with Delegate)
- 动机
- 做法
- 范例
- 参考
第 12 章 掌握继承的艺术:重构之路,化繁为简
12.0 前言
继承是面向对象编程中一把双刃剑,它强大而又易于被滥用。在软件开发的旅程中,我们常常会发现继承体系需要不断地调整和优化。本章将深入探讨一系列旨在处理继承关系、提升代码可读性和可维护性的重构手法。无论是函数和字段在继承体系中的上下移动,还是为继承体系添加或删除类,亦或是将继承转换为委托,这些技巧都将帮助你更好地驾驭继承的复杂性,让你的代码更加健壮和灵活。
12.1 函数上移(Pull Up Method)
反向重构: 函数下移(Push Down Method)
动机
代码重复是万恶之源。当多个子类中存在相同的函数实现时,不仅增加了维护成本,也埋下了未来引入 bug 的隐患。函数上移的目的是消除这种重复,将公共行为提升到它们的共同超类中。这不仅简化了代码,也使得未来的修改更加集中和安全。函数上移通常与其他重构(如函数参数化)结合使用,以使原本相似的函数变得完全一致,从而具备上移的条件。
做法
- 识别重复: 检查待提升的函数,确保它们在各子类中的逻辑和行为是完全一致的。
- 统一函数体(如果需要): 如果函数体不完全一致但逻辑相同,先对其进行重构(例如,使用函数参数化),直到它们变得完全相同。
- 检查依赖: 确保被提升函数体内引用的所有函数调用和字段都能在超类中访问到。如果不能,则需要先上移这些依赖项。
- 统一签名(如果需要): 如果待提升函数的签名不同,使用“改变函数声明”将它们的签名统一为你希望在超类中使用的签名。
- 在超类中创建函数: 在超类中新建一个函数,并将其中一个子类中的函数代码复制到超类中。
- 静态检查和测试: 对于静态语言,执行编译检查;对于动态语言,直接进行测试。
- 逐步移除子类函数: 移除一个待提升的子类函数。
- 测试: 运行测试以确保代码的正确性。
- 重复: 逐一移除剩余的子类函数,每次移除后都进行测试,直到只剩下超类中的函数。
- 处理抽象依赖(如果需要): 如果被提升的函数依赖于子类中特有的属性或方法,且这些属性或方法没有在超类中声明,你可能需要在超类中声明它们为抽象(abstract)方法或属性,以明确子类需要提供实现。
范例
假设我们有 Salesman
和 Engineer
两个类,它们都继承自 Employee
,并且各自有一个计算年成本的方法。
原始 JavaScript 代码:
class Employee {...}class Salesman extends Employee {get name() {...}
}class Engineer extends Employee {get name() {...}
}
现在我们来看 annualCost
方法的例子:
class Employee {// ...
}class Salesman extends Employee {constructor(monthlyCost) {super();this._monthlyCost = monthlyCost;}get annualCost() {return this._monthlyCost * 12;}
}class Engineer extends Employee {constructor(monthlyCost) {super();this._monthlyCost = monthlyCost;}get annualCost() {return this._monthlyCost * 12;}
}
重构后 Java 代码:
首先,我们发现 annualCost
方法在 Salesman
和 Engineer
中是完全相同的。同时,它们都依赖于 _monthlyCost
字段,这个字段目前在两个子类中都有定义。我们可以先使用“字段上移”将 _monthlyCost
提升到 Employee
超类中(这将在下一节详细介绍),或者直接将其作为抽象方法处理。这里假设 _monthlyCost
已经在 Employee
中声明或通过抽象方法访问。
// Employee 类可能已经有 monthlyCost 属性或者一个抽象方法来获取它
abstract class Employee {protected double monthlyCost; // 假设已经通过字段上移或构造函数设置// ... 其他属性和方法public double getAnnualCost() {// 在静态语言中,如果 monthlyCost 是子类特有的,这里需要通过抽象方法来获取// 或者像 JavaScript 例子那样, monthlyCost 最终会在超类中定义return getMonthlyCost() * 12;}// 如果 monthlyCost 是子类特有的,可以在超类中定义一个抽象方法public abstract double getMonthlyCost();// Smalltalk 风格的“子类责任”错误,在 Java 中通常通过抽象方法表达protected void subclassResponsibility() {throw new UnsupportedOperationException("Subclass must implement this method.");}
}class Salesman extends Employee {public Salesman(double monthlyCost) {this.monthlyCost = monthlyCost; // 设置 monthlyCost,或者通过构造函数参数传入}@Overridepublic double getMonthlyCost() {return monthlyCost;}// ... 其他 Salesman 特有的方法
}class Engineer extends Employee {public Engineer(double monthlyCost) {this.monthlyCost = monthlyCost; // 设置 monthlyCost}@Overridepublic double getMonthlyCost() {return monthlyCost;}// ... 其他 Engineer 特有的方法
}
解释:
- 我们将
getAnnualCost
方法从Salesman
和Engineer
中上移到Employee
超类。 - 为了让
Employee
能够计算annualCost
,它需要访问monthlyCost
。这里我选择了在Employee
中定义一个抽象方法getMonthlyCost()
,强制所有子类提供自己的monthlyCost
实现。这样,即使monthlyCost
是子类特有的数据,Employee
也能通过多态调用正确的方法。 - 在 Java 中,如果
monthlyCost
是所有Employee
都应该有的属性,那么它应该直接在Employee
类中声明(并可能通过构造函数初始化)。
12.2 字段上移(Pull Up Field)
反向重构: 字段下移(Push Down Field)
动机
当多个子类拥有相同或功能相似的字段时,这些重复的字段会增加代码的冗余,并可能导致数据不一致。字段上移旨在将这些公共字段提升到它们的共同超类中,从而减少重复,并使得与这些字段相关的行为(例如,通过“函数上移”提升的方法)也能被集中管理。
做法
- 识别重复: 针对待提升的字段,检查它们的所有使用点,确认它们以同样的方式被使用,即使名称可能不同。
- 统一字段名(如果需要): 如果这些字段的名称不同,先使用“变量改名”为它们取一个相同的名字。
- 在超类中新建字段: 在超类中声明一个新字段。
- 设置可见性: 新字段需要对所有子类可见(在大多数语言中,
protected
权限便已足够)。 - 移除子类中的字段: 从所有子类中删除该字段。
- 测试: 运行测试以确保代码的正确性。
范例
假设 Salesman
和 Engineer
都包含 name
字段。
原始 Java 代码:
class Employee {// ...
}class Salesman extends Employee {private String name; // 销售员的名字// ...
}class Engineer extends Employee {private String name; // 工程师的名字// ...
}
重构后 Java 代码:
class Employee {protected String name; // 员工的名字,对子类可见// ...public String getName() {return name;}
}class Salesman extends Employee {// name 字段已从子类移除// Salesman 的构造函数现在可以接受 name 参数并传递给 super()public Salesman(String name) {this.name = name; // 或者通过 super(name) 如果 Employee 有接受 name 的构造函数}// ...
}class Engineer extends Employee {// name 字段已从子类移除public Engineer(String name) {this.name = name; // 或者通过 super(name)}// ...
}
解释:
- 我们将
name
字段从Salesman
和Engineer
移动到了Employee
超类。 - 将
name
字段的访问修饰符设置为protected
,使其对子类可见,但对外部代码提供更好的封装性。 - 子类的构造函数现在可以通过直接访问
name
(如果超类中没有带参构造函数)或通过调用super(name)
来初始化这个字段。
12.3 构造函数本体上移(Pull Up Constructor Body)
动机
构造函数有其特殊的行为和限制,不能像普通函数那样直接使用“函数上移”。当多个子类的构造函数执行相似的初始化逻辑时,将这些共同逻辑提升到超类的构造函数中可以避免重复代码,并确保所有子类实例都能获得一致的基础初始化。
做法
- 确保超类构造函数存在: 如果超类还不存在构造函数,首先为其定义一个。确保子类调用超类的构造函数(例如,在 Java 中使用
super()
)。 - 移动公共语句: 使用“移动语句”将子类构造函数中的公共语句移动到
super()
调用之后。 - 逐步提升公共代码: 逐一移除子类构造函数间的公共代码,将其提升至超类构造函数中。对于公共代码中引用到的变量,将其作为参数传递给超类的构造函数。
- 测试: 每次修改后运行测试。
- 处理复杂公共代码: 如果存在无法简单提升至超类的复杂公共代码,先应用“提炼函数”,再利用“函数上移”提升之。
范例
假设我们有 Employee
和 Department
两个类,它们都继承自 Party
,并且在构造函数中都初始化了 name
属性。
原始 Java 代码:
class Party {// 假设 Party 初始是空的或只有一个默认构造函数
}class Employee extends Party {private String id;private String name;private double monthlyCost;public Employee(String name, String id, double monthlyCost) {// super(); // 隐式调用或显式调用无参构造函数this.id = id;this.name = name;this.monthlyCost = monthlyCost;}// ...
}class Department extends Party {private String name;private List<Employee> staff;public Department(String name, List<Employee> staff) {// super();this.name = name;this.staff = staff;}// ...
}
重构后 Java 代码:
-
移动
name
赋值到super()
之后(逻辑步骤):class Employee extends Party {public Employee(String name, String id, double monthlyCost) {super();this.name = name; // 移动到 super() 之后this.id = id;this.monthlyCost = monthlyCost;} }
-
将
name
的初始化提升到Party
的构造函数:class Party {protected String name; // 使用 protected 确保子类可访问public Party(String name) {this.name = name;} }class Employee extends Party {private String id;private double monthlyCost;public Employee(String name, String id, double monthlyCost) {super(name); // 调用超类的构造函数,并传入 namethis.id = id;this.monthlyCost = monthlyCost;}// ... }class Department extends Party {private List<Employee> staff;public Department(String name, List<Employee> staff) {super(name); // 调用超类的构造函数,并传入 namethis.staff = staff;}// ... }
处理复杂初始化逻辑(例如,条件初始化):
假设 Employee
和 Manager
(继承自 Employee
)都有一个 assignCar()
方法,并在构造函数中根据权限条件调用。
原始 Java 代码:
class Employee {protected String name;public Employee(String name) { this.name = name; }public boolean isPrivileged() { return false; } // 默认实现public void assignCar() { System.out.println(name + " assigned a basic car."); }
}class Manager extends Employee {private int grade;public Manager(String name, int grade) {super(name);this.grade = grade;if (this.isPrivileged()) { // 每个子类可能都有类似逻辑this.assignCar();}}@Overridepublic boolean isPrivileged() {return this.grade > 4;}
}
重构后 Java 代码:
-
在
Manager
中提炼finishConstruction
方法:class Manager extends Employee {private int grade;public Manager(String name, int grade) {super(name);this.grade = grade;finishConstruction(); // 调用提炼出的方法}private void finishConstruction() { // 提炼出的方法if (this.isPrivileged()) {this.assignCar();}}// ... }
-
将
finishConstruction
上移到Employee
超类:abstract class Employee { // 可能是抽象类,如果 isPrivileged 默认行为不完整protected String name;public Employee(String name) { this.name = name; }public abstract boolean isPrivileged(); // 强制子类实现public void assignCar() { System.out.println(name + " assigned a basic car."); }protected void finishConstruction() { // 上移到超类if (this.isPrivileged()) {this.assignCar();}} }class Manager extends Employee {private int grade;public Manager(String name, int grade) {super(name);this.grade = grade;finishConstruction(); // 调用超类中的方法}@Overridepublic boolean isPrivileged() {return this.grade > 4;} }
解释:
通过“构造函数本体上移”,我们可以在超类中集中处理共同的初始化逻辑,让子类的构造函数只关注自身特有的初始化。对于复杂的条件逻辑,可以先“提炼函数”再“函数上移”,从而保持代码的整洁。
12.4 函数下移(Push Down Method)
反向重构: 函数上移(Pull Up Method)
动机
当超类中的某个函数只与一个或少数几个子类相关,而对其他子类不适用时,这个函数最好被移动到真正需要它的子类中。这有助于保持超类的简洁性,使其接口只包含对所有子类都通用的行为。这也能避免超类中出现“空实现”或“抛出异常”的函数,这些都是继承体系设计不当的信号。
做法
- 复制函数: 将超类中的函数本体复制到每一个需要此函数的子类中。
- 删除超类函数: 从超类中删除该函数。
- 测试: 运行测试。
- 移除不必要子类函数: 将该函数从所有不需要它的那些子类中删除。
- 测试: 再次运行测试。
范例
假设 Employee
类有一个 getQuota()
方法,但这个方法实际上只对 Salesman
子类有意义,而 Engineer
子类不需要。
原始 Java 代码:
class Employee {// ...public double getQuota() {// 默认实现,可能返回0或者抛出异常return 0.0;}
}class Engineer extends Employee {// ...// Engineer 不需要 quota
}class Salesman extends Employee {// ...@Overridepublic double getQuota() {// Salesman 的具体配额逻辑return 10000.0;}
}
重构后 Java 代码:
-
将
getQuota()
复制到Salesman
: (已经存在,只需要确保超类中的实现是默认的或空的) -
从
Employee
中删除getQuota()
:class Employee {// getQuota() 方法已移除// ... }class Engineer extends Employee {// ...// Engineer 没有 getQuota() 方法 }class Salesman extends Employee {// ...public double getQuota() {return 10000.0;} }
解释:
通过“函数下移”,我们使 Employee
类更加专注于其核心的通用职责,而 Salesman
则明确拥有了其特有的 getQuota
行为。这使得继承体系的语义更加清晰,并且避免了在 Engineer
中出现一个无用的 getQuota
方法。
12.5 字段下移(Push Down Field)
反向重构: 字段上移(Pull Up Field)
动机
当超类中的某个字段只被一个子类或一小部分子类用到,而对其他子类没有意义时,这个字段最好被移动到真正需要它的子类中。这有助于保持超类的简洁性,避免超类中包含不必要的“通用”数据,使得超类更聚焦于其核心职责。
做法
- 在子类中声明字段: 在所有需要该字段的子类中声明该字段。
- 从超类中移除字段: 从超类中删除该字段。
- 测试: 运行测试。
- 移除不必要子类字段: 将该字段从所有不需要它的那些子类中删除(如果它最初被复制到多个子类,但后来发现某些子类不需要)。
- 测试: 再次运行测试。
范例
假设 Employee
类有一个 quota
字段,但这个字段实际上只对 Salesman
子类有意义,而 Engineer
子类不需要。
原始 Java 代码:
class Employee {protected double quota; // 配额,可能对所有 Employee 都默认定义// ...
}class Engineer extends Employee {// Engineer 不需要 quota,但它继承了// ...
}class Salesman extends Employee {// Salesman 使用 quota 字段// ...
}
重构后 Java 代码:
-
在
Salesman
中声明quota
字段:class Salesman extends Employee {protected double quota; // Salesman 拥有自己的 quota 字段// ... }
-
从
Employee
中移除quota
字段:class Employee {// quota 字段已移除// ... }class Engineer extends Employee {// Engineer 不再继承 quota 字段// ... }
解释:
通过“字段下移”,我们确保了 quota
字段只存在于真正需要它的 Salesman
类中。这使得 Employee
类更加精简,并且 Engineer
不再拥有一个它不需要的属性,从而提高了代码的清晰度和准确性。
12.6 以子类取代类型码(Replace Type Code with Subclasses)
包含旧重构: 以 State/Strategy 取代类型码(Replace Type Code with State/Strategy),提炼子类(Extract Subclass)
反向重构: 移除子类(Remove Subclass)
动机
在软件系统中,我们经常需要处理“相似但又不同”的对象。类型码(如枚举、字符串或数字)是表示这些分类的常见方式。然而,当类型码的值开始影响对象的行为(导致大量条件逻辑)或数据结构(某些字段只对特定类型码有意义)时,它就可能成为代码复杂性的来源。将类型码替换为子类,可以利用多态来消除条件逻辑,并通过将特有字段下移到子类来更清晰地表达数据与类型之间的关系。
做法
- 自封装类型码字段: 使用“封装变量”将类型码字段封装起来,通过
getter
和setter
访问。 - 创建子类并覆写类型码: 为类型码的每个取值创建一个子类。在每个子类中覆写类型码的
getter
方法,使其返回该类型码的字面量值。 - 创建选择器逻辑: 创建一个工厂方法或选择器逻辑,根据类型码参数映射到新的子类。
- 如果选择直接继承方案(子类直接继承原始类),用“以工厂函数取代构造函数”包装原始类的构造函数,将选择器逻辑放在工厂函数中。
- 如果选择间接继承方案(类型码本身成为一个对象,并从它继承出子类型),选择器逻辑可以保留在原始类的构造函数中,用于创建类型码的委托对象。
- 测试: 每次修改后执行测试。
- 重复: 针对每个类型码取值,重复上述“创建子类、添加选择器逻辑”的过程。
- 移除类型码字段: 当所有类型码都被子类取代后,可以删除原始类中的类型码字段。
- 测试: 运行测试。
- 处理相关函数: 使用“函数下移”和“以多态取代条件表达式”处理原本访问了类型码的函数。处理完毕后,可以移除类型码的访问函数。
范例
假设有一个 Employee
类,通过 type
字段来区分工程师、销售员和经理。
原始 Java 代码:
class Employee {private String name;private String type; // 类型码:engineer, manager, salesmanpublic Employee(String name, String type) {validateType(type);this.name = name;this.type = type;}private void validateType(String arg) {if (!List.of("engineer", "manager", "salesman").contains(arg)) {throw new IllegalArgumentException("Employee cannot be of type " + arg);}}public String getType() {return type;}@Overridepublic String toString() {return name + " (" + type + ")";}// 可能还有很多方法包含 if/else if/else 或 switch 语句,根据 type 进行不同的行为public double calculateBonus() {if (type.equals("engineer")) {return 1000;} else if (type.equals("salesman")) {return 2000;} else if (type.equals("manager")) {return 3000;}return 0;}
}
范例:直接继承
我们将采用直接继承方案,让 Engineer
, Salesman
, Manager
直接继承 Employee
。
-
自封装类型码字段:
class Employee {private String name;private String type; // 原始类型码字段public Employee(String name, String type) {validateType(type);this.name = name;this.type = type;}// ... validateType 方法不变public String getType() { // 封装后的 getterreturn type;}@Overridepublic String toString() {return name + " (" + getType() + ")"; // 使用 getter}// ... calculateBonus 方法不变 }
-
创建子类并覆写类型码:
class Engineer extends Employee {public Engineer(String name) {super(name, "engineer"); // 构造函数传入类型码}@Overridepublic String getType() {return "engineer"; // 覆写 getter,返回字面量值}// ... 可以覆写 calculateBonus 等方法@Overridepublic double calculateBonus() {return 1000;} }class Salesman extends Employee {public Salesman(String name) {super(name, "salesman");}@Overridepublic String getType() {return "salesman";}@Overridepublic double calculateBonus() {return 2000;} }class Manager extends Employee {public Manager(String name) {super(name, "manager");}@Overridepublic String getType() {return "manager";}@Overridepublic double calculateBonus() {return 3000;} }
-
创建工厂方法:
class EmployeeFactory { // 或者作为 Employee 的静态方法public static Employee createEmployee(String name, String type) {switch (type) {case "engineer":return new Engineer(name);case "salesman":return new Salesman(name);case "manager":return new Manager(name);default:throw new IllegalArgumentException("Unknown employee type: " + type);}} }
客户端现在会使用
EmployeeFactory.createEmployee("Alice", "engineer")
来创建员工对象。 -
移除类型码字段及相关逻辑:
现在Employee
类中的type
字段和validateType
方法变得多余,因为类型信息已经通过子类结构和工厂方法进行管理。abstract class Employee { // 声明为抽象类,因为它现在是基类protected String name; // protected 允许子类访问public Employee(String name) { // 构造函数不再需要 type 参数this.name = name;}// getType() 方法现在也可能被移除或保留为抽象方法public abstract String getType(); // 强制子类实现其类型@Overridepublic String toString() {return name + " (" + getType() + ")";}// calculateBonus 方法现在可以声明为抽象,由子类实现public abstract double calculateBonus(); }
解释:
通过“以子类取代类型码”,我们成功地将基于类型码的条件逻辑转换为了多态行为。EmployeeFactory
负责创建正确类型的子类实例,而每个子类则提供了其特有的行为实现。这大大简化了 Employee
类的逻辑,并提升了代码的扩展性和可维护性。
范例:间接继承 (以对象取代基本类型 + 以子类取代类型码)
有时候,原始类可能已经有了其他子类,或者类型码本身需要是可变的。这时,我们可以先用“以对象取代基本类型”将类型码封装成一个对象,然后对这个对象进行“以子类取代类型码”。
原始 Java 代码: (与直接继承的原始 Employee 类相同,但假设 Employee 已经有 FullTimeEmployee 和 PartTimeEmployee 子类)
// 假设 Employee 已经有了 FullTimeEmployee 和 PartTimeEmployee 子类
// class FullTimeEmployee extends Employee { ... }
// class PartTimeEmployee extends Employee { ... }class Employee {private String name;private String typeString; // 类型码字符串public Employee(String name, String typeString) {validateType(typeString);this.name = name;this.typeString = typeString;}private void validateType(String arg) {if (!List.of("engineer", "manager", "salesman").contains(arg)) {throw new IllegalArgumentException("Employee cannot be of type " + arg);}}public String getTypeString() {return typeString;}public void setTypeString(String typeString) {validateType(typeString);this.typeString = typeString;}public String getCapitalizedType() {return typeString.substring(0, 1).toUpperCase() + typeString.substring(1).toLowerCase();}@Overridepublic String toString() {return name + " (" + getCapitalizedType() + ")";}
}
-
以对象取代基本类型: 创建
EmployeeType
类来封装类型码。class EmployeeType {private String value;public EmployeeType(String value) {this.value = value;}@Overridepublic String toString() {return value;} }class Employee {private String name;private EmployeeType type; // 现在是一个对象public Employee(String name, String typeString) {validateType(typeString); // 验证原始字符串this.name = name;this.type = new EmployeeType(typeString); // 封装}private void validateType(String arg) { /* ... 验证逻辑不变 ... */ }public EmployeeType getType() {return type;}public void setType(String typeString) { // setter 仍接受字符串validateType(typeString);this.type = new EmployeeType(typeString);}public String getCapitalizedType() {return type.toString().substring(0, 1).toUpperCase() + type.toString().substring(1).toLowerCase();}@Overridepublic String toString() {return name + " (" + getCapitalizedType() + ")";} }
-
以子类取代类型码(针对
EmployeeType
):
现在我们针对EmployeeType
引入子类。abstract class EmployeeType { // 抽象的 EmployeeType 基类public abstract String toString(); // 强制子类实现其类型字符串public String getCapitalizedName() { // 移动公共行为到超类String typeName = toString();return typeName.substring(0, 1).toUpperCase() + typeName.substring(1).toLowerCase();} }class EngineerType extends EmployeeType {@Overridepublic String toString() { return "engineer"; } }class ManagerType extends EmployeeType {@Overridepublic String toString() { return "manager"; } }class SalesmanType extends EmployeeType {@Overridepublic String toString() { return "salesman"; } }class Employee {// ... (name 字段不变)private EmployeeType type; // 引用 EmployeeType 子类实例public Employee(String name, String typeString) {// validateType(typeString); // 验证逻辑现在在 createEmployeeType 中处理this.name = name;this.type = createEmployeeType(typeString); // 使用工厂方法}// 工厂方法用于创建正确的 EmployeeType 子类实例public static EmployeeType createEmployeeType(String typeString) {switch (typeString) {case "engineer": return new EngineerType();case "manager": return new ManagerType();case "salesman": return new SalesmanType();default: throw new IllegalArgumentException("Employee cannot be of type " + typeString);}}public EmployeeType getType() {return type;}public void setType(String typeString) { // 允许动态改变类型this.type = createEmployeeType(typeString);}public String getCapitalizedType() {return type.getCapitalizedName(); // 调用委托对象的方法}@Overridepublic String toString() {return name + " (" + getCapitalizedType() + ")";} }
解释:
通过间接继承,我们将类型码的职责从Employee
类中解耦出来。Employee
不再关心具体是哪种类型,而是委托给EmployeeType
对象。这使得Employee
类可以有其他维度的继承(如FullTimeEmployee
/PartTimeEmployee
),并且EmployeeType
也可以在运行时动态改变,提供了更大的灵活性。我们将getCapitalizedName
这样的公共行为上移到EmployeeType
超类,进一步减少了重复。
12.7 移除子类(Remove Subclass)
曾用名: 以字段取代子类(Replace Subclass with Fields)
反向重构: 以子类取代类型码(362)
当一个子类提供的差异性微乎其微,或者其存在的理由已不再成立时,它就可能成为代码负担。移除子类是一种精简代码、降低复杂度的有效手段,它用超类中的一个字段来代表原有的子类类型。
// Before Refactoring
class Person {private String name;public Person(String name) {this.name = name;}public String getName() {return name;}public String getGenderCode() {return "X"; // Default or unknown}
}class Male extends Person {public Male(String name) {super(name);}@Overridepublic String getGenderCode() {return "M";}
}class Female extends Person {public Female(String name) {super(name);}@Overridepublic String getGenderCode() {return "F";}
}// Client code example
// List<Person> people = ...;
// long numberOfMales = people.stream().filter(p -> p instanceof Male).count();
动机
子类是处理多样性和多态性的利器。但随着系统演化,子类所代表的差异性可能被消除,或者新的设计使其变得冗余。一个用处不大的子类只会增加阅读和理解代码的成本。通过移除子类,用超类中的字段替代,可以达到简化设计、提高代码可读性的目的。
做法
- 封装子类构造函数: 使用以工厂函数取代构造函数(334),为子类的创建提供一个统一的入口。如果子类从外部加载,可以将选择逻辑放在工厂函数中。
- 处理类型检查: 如果有代码通过
instanceof
运算符检查子类类型,使用**提炼函数(106)将其封装,然后用搬移函数(198)**将这个判断逻辑搬移到超类中。 - 新建类型字段: 在超类中新建一个字段,用于代表原先子类的类型。这个字段可以是枚举、字符串或其他合适的数据类型。
- 修改判断逻辑: 将所有基于子类类型的判断逻辑(包括第2步搬移到超类的判断逻辑)改为使用新建的类型字段。
- 删除子类: 在确认所有引用和行为都已正确迁移后,删除子类。
- 测试: 每完成一个阶段的修改后都进行测试。
范例
我们以一个 Person
类及其子类 Male
和 Female
为例。
// Before Refactoring (as shown above)
-
封装子类构造函数: 提炼一个工厂函数来创建
Person
对象。// Top-level / Utility class public class PersonFactory {public static Person createPerson(PersonRecord aRecord) {switch (aRecord.getGender()) {case "M": return new Male(aRecord.getName());case "F": return new Female(aRecord.getName());default: return new Person(aRecord.getName());}}public static List<Person> loadFromInput(List<PersonRecord> data) {return data.stream().map(PersonFactory::createPerson).collect(Collectors.toList());} }// PersonRecord is assumed to be a data structure holding name and gender. // Example: /* class PersonRecord {private String name;private String gender;// constructor, getters } */
-
处理类型检查: 封装
instanceof
检查并搬移到Person
类。// Before: // long numberOfMales = people.stream().filter(p -> p instanceof Male).count();// After moving isMale method to Person class Person {// ... existing code ...public boolean isMale() {return this instanceof Male;} }// Client code now: // long numberOfMales = people.stream().stream().filter(Person::isMale).count();
-
新建类型字段: 在
Person
类中添加_genderCode
字段。class Person {private String name;private String genderCode; // New field// Modified constructorpublic Person(String name, String genderCode) {this.name = name;this.genderCode = genderCode != null ? genderCode : "X"; // Default to "X"}// Original constructor will need to be updated or removed later.// For now, assume a factory handles this.public String getName() {return name;}public String getGenderCode() { // Now gets from the fieldreturn genderCode;}public boolean isMale() { // Will be updated in next stepreturn "M".equals(this.genderCode);} }
-
修改判断逻辑并更新工厂函数: 修改
createPerson
工厂函数,使其直接创建Person
对象,并传入性别代码。public class PersonFactory {public static Person createPerson(PersonRecord aRecord) {switch (aRecord.getGender()) {case "M": return new Person(aRecord.getName(), "M"); // Create Person directlycase "F": return new Person(aRecord.getName(), "F"); // Create Person directlydefault: return new Person(aRecord.getName(), "X");}}// ... loadFromInput method ... }// Update isMale method in Person class to use the new field class Person {// ... existing code ...public boolean isMale() {return "M".equals(this.genderCode);}// ... other methods ... }
-
删除子类: 删除
Male
和Female
类。// Male and Female classes are now removed.
至此,Male
和 Female
子类已被移除,其特有的行为通过 Person
类中的 genderCode
字段和相应逻辑来表示,大大简化了继承体系。
12.8 提炼超类(Extract Superclass)
当多个类做着相似的事情,并且拥有共同的数据和行为时,提炼超类是一个将这些共同点集中到一处的有效重构。它通过引入一个新的超类,让原有的类继承它,从而消除重复代码,提高代码的可维护性和可扩展性。
// Before Refactoring
class Department {private String name;private List<Employee> staff; // Assuming Employee class exists// ... constructor, getters ...public String getName() { /* ... */ return name; }public double getTotalMonthlyCost() { /* ... */ return staff.stream().mapToDouble(Employee::getMonthlyCost).sum(); }public int getHeadCount() { /* ... */ return staff.size(); }public double getTotalAnnualCost() {return getTotalMonthlyCost() * 12;}
}class Employee {private String id;private String name;private double monthlyCost;// ... constructor, getters ...public String getId() { /* ... */ return id; }public String getName() { /* ... */ return name; }public double getMonthlyCost() { /* ... */ return monthlyCost; }public double getAnnualCost() {return getMonthlyCost() * 12;}
}
动机
在系统演化过程中,相似的逻辑和数据可能分散在不同的类中,导致重复。提炼超类能够将这些共同元素上移到新的超类中,从而遵循 DRY(Don’t Repeat Yourself)原则。这不仅减少了代码量,也使得未来对共同行为的修改变得更加集中和容易。
做法
- 创建空白超类: 新建一个空白的超类,让所有需要提炼的类继承它。
- 调整构造函数(可选): 如果需要,使用**改变函数声明(124)**调整构造函数的签名,为后续字段上移做准备。
- 上移共同元素: 逐一使用构造函数本体上移(355)、函数上移(350)和字段上移(353),将子类的共同字段和函数上移到超类。
- 提炼并上移剩余共同逻辑: 检查子类中是否还有相似但未完全相同的函数。如有,先用**提炼函数(106)将其重构为相同函数,再用函数上移(350)**搬到超类。
- 调整客户端代码: 考虑将所有使用原有类的客户端代码调整为使用超类的接口,以利用多态性。
- 测试: 每完成一个阶段的修改后都进行测试。
范例
考虑 Employee
和 Department
这两个类,它们都有 name
属性,并且都涉及“成本”的概念。
// Before Refactoring (as shown above)
-
创建空白超类: 创建
Party
类,并让Employee
和Department
继承它。class Party {// Will be populated later }class Employee extends Party {// ... }class Department extends Party {// ... }
-
调整构造函数并上移
name
字段: 使用**字段上移(353)和构造函数本体上移(355)**将name
字段和初始化逻辑移到Party
类。class Party {protected String name; // Use protected for subclass visibilitypublic Party(String name) {this.name = name;}public String getName() {return name;} }class Employee extends Party {private String id;private double monthlyCost;public Employee(String name, String id, double monthlyCost) {super(name); // Call superclass constructorthis.id = id;this.monthlyCost = monthlyCost;}// getName() is now inherited from Party// ... other methods ... }class Department extends Party {private List<Employee> staff;public Department(String name, List<Employee> staff) {super(name); // Call superclass constructorthis.staff = staff;}// getName() is now inherited from Party// ... other methods ... }
-
上移
annualCost
相关函数:Employee
的getAnnualCost()
和Department
的getTotalAnnualCost()
逻辑相似。首先,用**改变函数声明(124)**统一Department
中计算月度成本的方法名,使其与Employee
的getMonthlyCost()
意图一致,例如改为getMonthlyCost()
。class Department extends Party {// ... existing fields and methods ...// Change getTotalMonthlyCost to getMonthlyCost for consistencypublic double getMonthlyCost() { // Renamed from getTotalMonthlyCostreturn staff.stream().mapToDouble(Employee::getMonthlyCost).sum();}// Change getTotalAnnualCost to getAnnualCostpublic double getAnnualCost() { // Renamed from getTotalAnnualCostreturn getMonthlyCost() * 12; // Now calls Department's getMonthlyCost}// ... }
现在
Employee
和Department
都有一个名为getMonthlyCost()
(或其语义等价物)的方法和一个名为getAnnualCost()
的方法,后者都是getMonthlyCost() * 12
。我们可以将getAnnualCost()
上移到Party
类。abstract class Party { // Now Party can be abstract as it defines abstract methodsprotected String name;public Party(String name) {this.name = name;}public String getName() {return name;}// Abstract method: subclasses must implement how to get their monthly costpublic abstract double getMonthlyCost();public double getAnnualCost() {return getMonthlyCost() * 12;} }class Employee extends Party {private String id;private double monthlyCost;public Employee(String name, String id, double monthlyCost) {super(name);this.id = id;this.monthlyCost = monthlyCost;}public String getId() { return id; }@Overridepublic double getMonthlyCost() { // Implementation for Employeereturn monthlyCost;}// getAnnualCost() is now inherited and uses Employee's getMonthlyCost() }class Department extends Party {private List<Employee> staff;public Department(String name, List<Employee> staff) {super(name);this.staff = staff;}public List<Employee> getStaff() { return new ArrayList<>(staff); } // Defensive copy@Overridepublic double getMonthlyCost() { // Implementation for Departmentreturn staff.stream().mapToDouble(Employee::getMonthlyCost).sum();}// getAnnualCost() is now inherited and uses Department's getMonthlyCost() }
通过这些步骤,我们成功地提炼出了一个 Party
超类,它承载了 name
属性和通用的 getAnnualCost
逻辑,同时通过抽象方法强制子类实现 getMonthlyCost
,从而实现了行为的复用和抽象。
12.9 折叠继承体系(Collapse Hierarchy)
当一个类与其超类(或子类)之间的差异变得微不足道,不再值得作为独立的类存在时,折叠继承体系就是将它们合并为单个类的有效手段。这有助于简化设计,减少不必要的抽象层级。
// Before Refactoring
class Employee {// ... common employee details ...public String getRole() { return "General Employee"; }
}class Salesman extends Employee {private double quota;// ... constructor, getters ...@Overridepublic String getRole() { return "Salesman"; }
}// After Refactoring
class Employee {// ... common employee details ...private String role; // New field to represent the role// ... constructor, getters ...public Employee(String name, String role, double quota) {// ...this.role = role;if ("Salesman".equals(role)) {this.quota = quota; // Initialize salesman-specific data}}public String getRole() { return role; }public double getQuota() {if ("Salesman".equals(role)) {return quota;}throw new UnsupportedOperationException("Only salesmen have quotas.");}
}
动机
随着代码演化,原本有意义的继承关系可能变得多余。例如,子类特有的字段和方法可能被移动到超类中,或者子类的行为变得与超类几乎完全一致。不必要的继承层级会增加理解成本,并可能导致过度设计。折叠继承体系通过合并类来去除这些冗余,使设计更加简洁。
做法
- 选择目标类: 决定要保留超类还是子类。通常选择一个名称在未来更有意义的类。
- 迁移所有元素: 使用字段上移(353)、字段下移(361)、函数上移(350)和函数下移(359),将所有相关字段和方法移动到目标类中。
- 调整引用点: 调整所有对即将被移除的类的引用,使其指向合并后保留的类。
- 移除空类: 删除已经变为空白的目标类。
- 测试: 每完成一个阶段的修改后都进行测试。
范例
考虑一个简单的 Employee
类和它的子类 Salesman
。如果 Salesman
除了一个 quota
字段和覆写的 getRole()
方法外,没有其他显著差异,那么就可以考虑将其折叠到 Employee
中。
// Before Refactoring (as shown above)
-
选择目标类: 我们选择保留
Employee
类,并将Salesman
的功能合并到其中。 -
迁移所有元素:
- 字段下移(361)/字段上移(353)的应用:
Salesman
的quota
字段需要被吸收到Employee
中。同时,Salesman
覆写了getRole()
方法,这意味着Employee
需要一个字段来存储这个角色信息。
class Employee {private String name;private String role; // New field to hold the role informationprivate double quota; // Field to hold salesman's quota, potentially null/default for otherspublic Employee(String name, String role) {this(name, role, 0.0); // Default quota}public Employee(String name, String role, double quota) {this.name = name;this.role = role;if ("Salesman".equals(role)) {this.quota = quota;} else {this.quota = 0.0; // Or throw exception, or mark as not applicable}}public String getName() { return name; }public String getRole() { return role; }public double getQuota() {if (!"Salesman".equals(role)) {throw new UnsupportedOperationException("Only salesmen have quotas.");}return quota;} }
- 函数下移(359)/函数上移(350)的应用:
Salesman
的getRole()
方法被合并到Employee
的getRole()
中,通过role
字段来动态返回。如果Salesman
有其他特有的方法,也需要搬移到Employee
类中,并通过role
字段进行条件判断。
- 字段下移(361)/字段上移(353)的应用:
-
调整引用点: 找出所有创建
Salesman
实例的地方,改为创建Employee
实例,并传入相应的role
参数。// Before: // Employee employee = new Salesman("Alice", 10000.0);// After: Employee employee = new Employee("Alice", "Salesman", 10000.0);
-
移除空类: 删除
Salesman
类。// Salesman class is now removed.
通过这些步骤,Salesman
类被成功折叠到 Employee
类中,简化了继承体系,同时保留了所有必要的信息和行为。
12.10 以委托取代子类(Replace Subclass with Delegate)
曾用名: 以 State/Strategy 取代类型码(Replace Type Code with State/Strategy),提炼子类(Extract Subclass)
当一个对象的行为有明显的类别之分,但继承关系带来了限制(如单继承、紧密耦合),或者需要动态改变对象的类型时,以委托取代子类是一个非常有用的重构。它将子类特有的行为转移到一个独立的委托对象中,并通过超类持有委托对象的引用,从而实现行为的多态性和灵活性。
// Before Refactoring
class Order {// ... common order details ...private Warehouse warehouse; // Assuming Warehouse class existspublic Order(Warehouse warehouse) {this.warehouse = warehouse;}public int getDaysToShip() {return warehouse.getDaysToShip();}
}class PriorityOrder extends Order {private PriorityPlan priorityPlan; // Assuming PriorityPlan class existspublic PriorityOrder(Warehouse warehouse, PriorityPlan priorityPlan) {super(warehouse);this.priorityPlan = priorityPlan;}@Overridepublic int getDaysToShip() {return priorityPlan.getDaysToShip(); // Overrides default shipping days}
}// Client code:
// Order normalOrder = new Order(new Warehouse());
// Order priorityOrder = new PriorityOrder(new Warehouse(), new PriorityPlan());
动机
继承是处理类别差异的自然方式,但它有局限性:
- 单继承限制: 只能继承一次,无法同时处理多个维度的变化。
- 紧密耦合: 超类和子类之间耦合紧密,超类的改变可能意外破坏子类。
- 动态类型变更: 难以在运行时改变对象的类型(例如,将普通订单升级为加急订单)。
委托解决了这些问题。它允许对象将部分职责委托给其他对象,从而实现更灵活、解耦的设计。当继承关系导致僵硬或复杂时,考虑使用委托。
做法
- 封装构造函数: 如果构造函数有多个调用者,使用**以工厂函数取代构造函数(334)**统一对象的创建入口。
- 创建委托类: 创建一个空的委托类,其构造函数接受子类特有的数据项,并通常接受一个指向超类的反向引用(
host
或owner
)。 - 超类添加委托字段: 在超类中添加一个字段,用于存放委托对象。
- 初始化委托字段: 修改子类的创建逻辑(在工厂函数或构造函数中),使其初始化委托字段,放入一个委托对象的实例。
- 搬移子类函数: 逐一选择子类中的函数,使用**搬移函数(198)**将其移入委托类。
- 在委托类中,将原先访问超类数据的代码改为通过反向引用(
host
)访问。 - 在子类中,修改原函数,使其将调用请求转发给委托对象。
- 如果需要,在超类中添加分发逻辑,检查委托对象是否存在,并转发调用。
- 如果委托类之间有重复代码,使用**提炼超类(375)**消除重复。
- 在委托类中,将原先访问超类数据的代码改为通过反向引用(
- 删除子类: 当所有子类函数都搬移完毕后,将所有调用子类构造函数的地方改为调用超类构造函数,并最终删除子类。
- 测试: 每完成一个阶段的修改后都进行测试。
范例
我们以 Order
和 PriorityOrder
为例,PriorityOrder
覆写了 getDaysToShip()
方法。
// Before Refactoring (as shown above)
-
封装构造函数:
// Top-level / Utility class public class OrderFactory {public static Order createOrder(Warehouse warehouse) {return new Order(warehouse);}public static Order createPriorityOrder(Warehouse warehouse, PriorityPlan priorityPlan) {return new PriorityOrder(warehouse, priorityPlan);} }// Client code now uses OrderFactory // Order normalOrder = OrderFactory.createOrder(new Warehouse()); // Order priorityOrder = OrderFactory.createPriorityOrder(new Warehouse(), new PriorityPlan());
-
创建委托类:
class PriorityOrderDelegate {private Order hostOrder; // Back-reference to the host Orderprivate PriorityPlan priorityPlan;public PriorityOrderDelegate(Order hostOrder, PriorityPlan priorityPlan) {this.hostOrder = hostOrder;this.priorityPlan = priorityPlan;}// Will move methods here }
-
超类添加委托字段并初始化: 在
Order
中添加priorityDelegate
字段,并在工厂函数中初始化。class Order {private Warehouse warehouse;private PriorityOrderDelegate priorityDelegate; // New fieldpublic Order(Warehouse warehouse) {this.warehouse = warehouse;}// Method to "be premium" - could be public if dynamic change is desiredvoid bePremium(PriorityPlan priorityPlan) {this.priorityDelegate = new PriorityOrderDelegate(this, priorityPlan);}public int getDaysToShip() {// Will add dispatch logic hereif (priorityDelegate != null) {return priorityDelegate.getDaysToShip();}return warehouse.getDaysToShip();} }public class OrderFactory {// ... createOrder ...public static Order createPriorityOrder(Warehouse warehouse, PriorityPlan priorityPlan) {Order order = new Order(warehouse); // Create a normal Orderorder.bePremium(priorityPlan); // Make it premium via delegatereturn order;} }
-
搬移子类函数: 将
PriorityOrder
的getDaysToShip()
方法搬移到PriorityOrderDelegate
。class PriorityOrderDelegate {// ... constructor ...@Override // Or just define itpublic int getDaysToShip() {return priorityPlan.getDaysToShip();} }// The dispatch logic is already in Order.getDaysToShip() // public int getDaysToShip() { // if (priorityDelegate != null) { // return priorityDelegate.getDaysToShip(); // } // return warehouse.getDaysToShip(); // }
-
删除子类: 删除
PriorityOrder
类。// PriorityOrder class is now removed.
通过这个重构,PriorityOrder
的特有行为被封装到 PriorityOrderDelegate
中。Order
类通过持有一个 PriorityOrderDelegate
实例来获得“高级”行为。这不仅解耦了 Order
和 PriorityOrder
,还使得 Order
对象可以在运行时动态地切换其“优先级”状态,例如通过调用 order.bePremium(new PriorityPlan())
。
范例:取代继承体系
当整个继承体系都因为上述原因变得僵硬时,我们可以将其替换为委托体系。这涉及到为每个子类创建一个对应的委托类,并构建一个委托类的继承体系来管理它们的共同行为。
// Before Refactoring (as shown in original text)
// Bird, EuropeanSwallow, AfricanSwallow, NorwegianBlueParrot classes and createBird factory
假设我们有 Bird
、EuropeanSwallow
、AfricanSwallow
和 NorwegianBlueParrot
等类。现在需要引入 WildBird
和 CaptiveBird
的差异,但单继承限制了我们。
-
创建委托超类和子类:
// Delegate for all bird species abstract class SpeciesDelegate {protected Bird bird; // Back-reference to the host Birdpublic SpeciesDelegate(Bird bird) {this.bird = bird;}public String getPlumage() {return bird.getPlumageProperty() != null ? bird.getPlumageProperty() : "average"; // Default or inherited}public int getAirSpeedVelocity() {return 0; // Default or abstract} }class EuropeanSwallowDelegate extends SpeciesDelegate {public EuropeanSwallowDelegate(Bird bird) { super(bird); }@Overridepublic int getAirSpeedVelocity() { return 35; } }class AfricanSwallowDelegate extends SpeciesDelegate {private int numberOfCoconuts;public AfricanSwallowDelegate(Bird bird, int numberOfCoconuts) {super(bird);this.numberOfCoconuts = numberOfCoconuts;}@Overridepublic int getAirSpeedVelocity() { return 40 - 2 * numberOfCoconuts; } }class NorwegianBlueParrotDelegate extends SpeciesDelegate {private int voltage;private boolean isNailed;public NorwegianBlueParrotDelegate(Bird bird, int voltage, boolean isNailed) {super(bird);this.voltage = voltage;this.isNailed = isNailed;}@Overridepublic int getAirSpeedVelocity() { return isNailed ? 0 : 10 + voltage / 10; }@Overridepublic String getPlumage() {return voltage > 100 ? "scorched" : (bird.getPlumageProperty() != null ? bird.getPlumageProperty() : "beautiful");} }
-
Bird
类引入委托字段和选择逻辑:class Bird {private String name;private String plumageProperty; // Original plumage fieldprivate SpeciesDelegate speciesDelegate; // New delegate fieldpublic Bird(String name, String plumageProperty, String type, int numberOfCoconuts, int voltage, boolean isNailed) {this.name = name;this.plumageProperty = plumageProperty;this.speciesDelegate = selectSpeciesDelegate(type, numberOfCoconuts, voltage, isNailed);}private SpeciesDelegate selectSpeciesDelegate(String type, int numberOfCoconuts, int voltage, boolean isNailed) {switch (type) {case "EuropeanSwallow": return new EuropeanSwallowDelegate(this);case "AfricanSwallow": return new AfricanSwallowDelegate(this, numberOfCoconuts);case "NorwegianBlueParrot": return new NorwegianBlueParrotDelegate(this, voltage, isNailed);default: return new DefaultSpeciesDelegate(this); // A default delegate for unknown types}}// Getters for name, plumage, airspeedpublic String getName() { return name; }public String getPlumageProperty() { return plumageProperty; } // Expose for delegate accesspublic String getPlumage() { return speciesDelegate.getPlumage(); }public int getAirSpeedVelocity() { return speciesDelegate.getAirSpeedVelocity(); } }// A simple DefaultSpeciesDelegate might be needed class DefaultSpeciesDelegate extends SpeciesDelegate {public DefaultSpeciesDelegate(Bird bird) { super(bird); } }
-
更新工厂函数和客户端代码:
public class BirdFactory {public static Bird createBird(BirdData data) {return new Bird(data.getName(), data.getPlumage(), data.getType(),data.getNumberOfCoconuts(), data.getVoltage(), data.isNailed());} }// BirdData class (or similar DTO) /* class BirdData {private String name;private String plumage;private String type;private int numberOfCoconuts;private int voltage;private boolean isNailed;// constructor, getters } */
-
删除所有旧的子类:
EuropeanSwallow
,AfricanSwallow
,NorwegianBlueParrot
类将被删除。
这个例子展示了如何用一个委托体系取代原有继承体系,使得 Bird
类可以重新用于其他继承维度(如 WildBird
/ CaptiveBird
),同时保持了品种特有行为的封装和多态性。
12.11 以委托取代超类(Replace Superclass with Delegate)
曾用名:以委托取代继承(Replace Inheritance with Delegation)
动机
在面向对象编程中,继承是一种强大且便捷的代码复用机制。通过继承现有类,我们可以覆写(override)部分功能或添加新功能,从而实现代码复用。然而,继承并非万能药,它也可能导致一些问题和混乱。
一个经典的继承误用案例是让栈(Stack)继承列表(List)。初衷是为了复用列表类的数据存储和操作能力,但问题在于列表类的所有操作都会暴露在栈类的接口上,而其中大部分操作对栈而言并不适用(例如,栈不应该允许随机访问或插入)。更好的做法是将列表作为栈的一个内部字段,并将栈所需的操作委托给列表来完成。
这正是“以委托取代超类”重构手法的用武之地。如果超类的一些功能对子类并不完全适用,或者子类的所有实例并非严格意义上的超类实例,就意味着我们不应该通过继承来获得超类的功能。
合理的继承关系应满足两个重要特征:
- 子类应能使用超类的所有函数: 超类提供的方法对子类来说都应该是语义上合理的。
- 子类的所有实例都应该是超类的实例: 即满足“是一个(is-a)”关系。通过超类的接口来使用子类的实例不应该出现任何问题。例如,如果有一个
CarModel
类,包含名称、引擎大小等属性,我们可能想让RealCar
类继承它并添加 VIN 码、制造日期等属性。但从语义上讲,一辆真实的汽车并不是一个模型。这是一种常见的建模错误,我们称之为“类型与实例名不符实”(type-instance homonym)。
在上述两种情况下,有问题的继承关系会引入混乱和潜在的错误。通过将继承关系转换为将部分职责委托给另一个对象,这些混乱和错误通常可以轻松避免。委托关系能更清晰地表达“这是一个不同的东西,我只是需要用到其中携带的一些功能”的意图。
尽管委托会引入一些额外的复杂性(例如,对于原本超类和子类中相同的函数,现在需要在宿主类中编写转发函数),但这些转发函数通常非常简单,出错的可能性极低。
有人会建议完全避免使用继承,但我并不认同。如果符合继承关系的语义条件,继承仍然是一种简洁且高效的复用机制。如果情况发生变化,继承不再是最佳选择,我们也可以相对容易地运用“以委托取代超类”进行重构。因此,我的建议是:首先(尽量)使用继承,如果发现继承有问题,再使用以委托取代超类。
做法
- 在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的新实例。
- 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发给委托引用。 每转发一块完整逻辑(例如一个完整的 getter 或 setter),都要执行测试。大多数情况下,每转发一个函数就可以测试;但一对设值/取值函数必须同时转移,然后才能测试。
- 当所有超类函数都被转发函数覆写后,就可以去掉继承关系。
范例
我最近给一个古城里存放上古卷轴(scroll)的图书馆做了咨询。他们给卷轴的信息编制了一份目录(catalog),每份卷轴都有一个 ID 号,并记录了卷轴的标题(title)和一系列标签(tag)。
CatalogItem
类 (Java)
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;public class CatalogItem {private String id;private String title;private Set<String> tags;public CatalogItem(String id, String title, Set<String> tags) {this.id = id;this.title = title;this.tags = new HashSet<>(tags); // defensive copy}public String getId() {return id;}public String getTitle() {return title;}public boolean hasTag(String arg) {return tags.contains(arg);}public Set<String> getTags() {return Collections.unmodifiableSet(tags); // return unmodifiable set}
}
这些古老的卷轴需要日常清扫,因此代表卷轴的 Scroll
类继承了代表目录项的 CatalogItem
类,并扩展出与“需要清扫”相关的数据。
Scroll
类 (Java, 初始继承版本)
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Set;public class Scroll extends CatalogItem {private LocalDate lastCleaned;public Scroll(String id, String title, Set<String> tags, LocalDate dateLastCleaned) {super(id, title, tags);this.lastCleaned = dateLastCleaned;}public boolean needsCleaning(LocalDate targetDate) {long threshold = this.hasTag("revered") ? 700 : 1500;return daysSinceLastCleaning(targetDate) > threshold;}public long daysSinceLastCleaning(LocalDate targetDate) {return lastCleaned.until(targetDate, ChronoUnit.DAYS);}
}
这就是一个常见的建模错误。真实存在的卷轴和只存在于纸面上的目录项是完全不同的两种东西。例如,关于“如何治疗灰鳞病”的卷轴可能有好几卷,但在目录上却只记录一个条目。
这样的建模错误很多时候可以置之不理。像“标题”和“标签”这样的数据,我可以认为是目录中数据的副本。如果这些数据从不发生改变,我完全可以接受这样的表现形式。但如果需要更新其中某处数据,我就必须非常小心,确保同一个目录项对应的所有数据副本都被正确地更新。
就算没有数据更新的问题,我还是希望改变这两个类之间的关系。把“目录项”作为“卷轴”的超类很可能会把未来的程序员搞迷糊,因此这是一个糟糕的模型。
重构步骤:以委托取代超类
1. 在 Scroll
类中新建一个字段,使其引用 CatalogItem
的一个对象
首先,在 Scroll
类中添加一个 _catalogItem
字段,并将其初始化为 CatalogItem
的新实例。
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Set;public class Scroll extends CatalogItem { // Still inherits CatalogItem for nowprivate CatalogItem catalogItem; // New field for delegationprivate LocalDate lastCleaned;public Scroll(String id, String title, Set<String> tags, LocalDate dateLastCleaned) {super(id, title, tags); // Call to super constructor remains for nowthis.catalogItem = new CatalogItem(id, title, tags); // Initialize the delegatethis.lastCleaned = dateLastCleaned;}public boolean needsCleaning(LocalDate targetDate) {long threshold = this.hasTag("revered") ? 700 : 1500; // Still using inherited hasTagreturn daysSinceLastCleaning(targetDate) > threshold;}public long daysSinceLastCleaning(LocalDate targetDate) {return lastCleaned.until(targetDate, ChronoUnit.DAYS);}
}
说明: 此时 Scroll
类同时拥有继承自 CatalogItem
的特性和通过 catalogItem
字段委托的特性。这是重构过程中的临时状态,确保在移除继承之前所有功能都被正确转移。
2. 为超类的每个函数创建转发函数
接下来,对于 CatalogItem
中的每个公共函数(getId()
, getTitle()
, hasTag()
),在 Scroll
类中创建对应的转发函数,将调用请求转发给 _catalogItem
字段。
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Set;public class Scroll extends CatalogItem {private CatalogItem catalogItem;private LocalDate lastCleaned;public Scroll(String id, String title, Set<String> tags, LocalDate dateLastCleaned) {super(id, title, tags);this.catalogItem = new CatalogItem(id, title, tags);this.lastCleaned = dateLastCleaned;}// Forwarding methods for CatalogItem's public methods@Override // Mark for clarity, though not strictly necessary if not overriding directlypublic String getId() {return catalogItem.getId();}@Overridepublic String getTitle() {return catalogItem.getTitle();}@Overridepublic boolean hasTag(String arg) {return catalogItem.hasTag(arg);}// Original Scroll methodspublic boolean needsCleaning(LocalDate targetDate) {// Now uses the forwarded hasTaglong threshold = this.hasTag("revered") ? 700 : 1500;return daysSinceLastCleaning(targetDate) > threshold;}public long daysSinceLastCleaning(LocalDate targetDate) {return lastCleaned.until(targetDate, ChronoUnit.DAYS);}
}
说明: Scroll
类现在通过委托对象 catalogItem
来响应 getId()
、getTitle()
和 hasTag()
等调用。needsCleaning
方法也因此使用了委托后的 hasTag
。在每完成一个转发函数后,都应该运行测试以确保功能正常。
3. 去掉继承关系
当所有超类函数都被转发函数覆盖后,就可以安全地移除 Scroll
与 CatalogItem
之间的继承关系。
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Set;// Scroll no longer extends CatalogItem
public class Scroll {private CatalogItem catalogItem;private LocalDate lastCleaned;public Scroll(String id, String title, Set<String> tags, LocalDate dateLastCleaned) {// No super() call needed anymorethis.catalogItem = new CatalogItem(id, title, tags);this.lastCleaned = dateLastCleaned;}// Forwarding methods remainpublic String getId() {return catalogItem.getId();}public String getTitle() {return catalogItem.getTitle();}public boolean hasTag(String arg) {return catalogItem.hasTag(arg);}// Original Scroll methodspublic boolean needsCleaning(LocalDate targetDate) {long threshold = this.hasTag("revered") ? 700 : 1500;return daysSinceLastCleaning(targetDate) > threshold;}public long daysSinceLastCleaning(LocalDate targetDate) {return lastCleaned.until(targetDate, ChronoUnit.DAYS);}
}
说明: 此时 Scroll
类完全独立于 CatalogItem
。它通过聚合 CatalogItem
对象并转发调用来复用其功能,而不是通过继承。
收尾工作:处理
Scroll
自身的 ID 与CatalogItem
的 ID
基本“以委托取代超类”重构到这里就完成了,不过在这个例子中,我还有一点收尾工作要做。
前面的重构把 CatalogItem
变成了 Scroll
的一个组件:每个 Scroll
对象包含一个独一无二的 CatalogItem
对象。在使用本重构的很多情况下,这样处理就够了。但在这个例子中,更好的建模方式应该是:关于灰鳞病的一个目录项,对应于图书馆中的 6 份卷轴,因为这 6 份卷轴都是同一个标题。这实际上是要运用将值对象改为引用对象(Replace Value with Reference)。
但在使用将值对象改为引用对象之前,还有一个问题需要先修好。在原来的继承结构中,Scroll
类使用了 CatalogItem
类的 id
字段来保存自己的 ID。但如果我把 CatalogItem
当作引用来处理,那么透过这个引用获得的 ID 就应该是目录项的 ID,而不是卷轴的 ID。也就是说,我需要在 Scroll
类上添加自己的 id
字段,在创建 Scroll
对象时使用这个字段,而不是使用来自 CatalogItem
类的 id
字段。这一步既可以说是搬移,也可以说是拆分。
1. Scroll
拥有自己的 id
字段
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;public class Scroll {private String id; // Scroll's own IDprivate CatalogItem catalogItem;private LocalDate lastCleaned;public Scroll(String id, String title, Set<String> tags, LocalDate dateLastCleaned) {this.id = id; // Initialize Scroll's own IDthis.catalogItem = new CatalogItem(null, title, tags); // CatalogItem might not have its own ID yet, or it's a dummy for nowthis.lastCleaned = dateLastCleaned;}@Override // Still overriding, but now it's Scroll's own IDpublic String getId() {return this.id;}// Forwarding methods for CatalogItem's public methodspublic String getTitle() {return catalogItem.getTitle();}public boolean hasTag(String arg) {return catalogItem.hasTag(arg);}// Original Scroll methodspublic boolean needsCleaning(LocalDate targetDate) {long threshold = this.hasTag("revered") ? 700 : 1500;return daysSinceLastCleaning(targetDate) > threshold;}public long daysSinceLastCleaning(LocalDate targetDate) {return lastCleaned.until(targetDate, ChronoUnit.DAYS);}
}
说明: Scroll
现在有了自己的 id
字段,并且 getId()
方法返回的是 Scroll
自身的 ID。在创建 CatalogItem
时,我们暂时传入 null
作为 ID,这通常在重构过程中是可以接受的临时状态。
2. 加载程序调整与 Scroll
构造函数修改(将值对象改为引用对象)
当前 Scroll
对象是从一个加载程序中加载的。
Loader
类 (Java)
import java.time.LocalDate;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;class Record {String id;CatalogData catalogData;String lastCleaned;static class CatalogData {String id; // Catalog Item IDString title;Set<String> tags;public CatalogData(String id, String title, Set<String> tags) {this.id = id;this.title = title;this.tags = tags;}}public Record(String id, CatalogData catalogData, String lastCleaned) {this.id = id;this.catalogData = catalogData;this.lastCleaned = lastCleaned;}
}public class Loader {// A simple mock for catalog repositoryprivate static Map<String, CatalogItem> catalogRepository = new HashMap<>();public static List<Scroll> loadScrolls(List<Record> records) {// Assume catalog is pre-loaded into the repository// Example:records.stream().map(r -> r.catalogData).distinct() // Process unique catalog items.forEach(cd -> {if (!catalogRepository.containsKey(cd.id)) {catalogRepository.put(cd.id, new CatalogItem(cd.id, cd.title, cd.tags));}});// Original loading logic (before passing catalog and catalogID)/*return records.stream().map(record -> new Scroll(record.id,record.catalogData.title,record.catalogData.tags,LocalDate.parse(record.lastCleaned))).collect(Collectors.toList());*/// Adjusted loading logic to pass catalogID and catalog repositoryreturn records.stream().map(record -> new Scroll(record.id,record.catalogData.title,record.catalogData.tags,LocalDate.parse(record.lastCleaned),record.catalogData.id,catalogRepository)) // Pass catalog ID and repository.collect(Collectors.toList());}
}
说明: Record
类和 Loader
类是模拟数据加载的辅助类。我假设 CatalogItem
有一个 ID 并在仓库中被管理。
将值对象改为引用对象(256)的第一步是要找到或者创建一个仓库对象(repository)。我发现有一个仓库对象可以很容易地导入加载程序中,这个仓库对象负责提供 CatalogItem
对象,并用 ID 作为索引。我的下一项任务就是要想办法把这个 ID 值放进 Scroll
对象的构造函数。还好,输入数据中有这个值,不过之前一直被无视了,因为在使用继承的时候用不着。把这些信息都理清楚,我就可以运用改变函数声明(124),把整个目录对象以及目录项的 ID 都作为参数传给 Scroll
的构造函数。
Scroll
类的构造函数调整 (Java)
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Map; // For the catalog repository
import java.util.Set;public class Scroll {private String id;private CatalogItem catalogItem;private LocalDate lastCleaned;// Adjusted constructor to receive catalogID and the catalog repositorypublic Scroll(String id, String title, Set<String> tags, LocalDate dateLastCleaned, String catalogID, Map<String, CatalogItem> catalogRepository) {this.id = id;// Fetch the CatalogItem from the repository using catalogIDthis.catalogItem = catalogRepository.get(catalogID);if (this.catalogItem == null) {// Handle error or create new if not found (depending on domain rules)// For simplicity, let's assume it always exists in this examplethrow new IllegalArgumentException("Catalog item with ID " + catalogID + " not found.");}this.lastCleaned = dateLastCleaned;}public String getId() {return this.id;}// Forwarding methods for CatalogItem's public methodspublic String getTitle() {return catalogItem.getTitle();}public boolean hasTag(String arg) {return catalogItem.hasTag(arg);}// ... other forwarding methods for CatalogItem if any ...// Original Scroll methodspublic boolean needsCleaning(LocalDate targetDate) {long threshold = this.hasTag("revered") ? 700 : 1500;return daysSinceLastCleaning(targetDate) > threshold;}public long daysSinceLastCleaning(LocalDate targetDate) {return lastCleaned.until(targetDate, ChronoUnit.DAYS);}
}
说明: Scroll
的构造函数现在接收 catalogID
和 catalogRepository
。它不再创建新的 CatalogItem
,而是从仓库中获取引用。这意味着多个 Scroll
对象可以引用同一个 CatalogItem
实例,实现了将值对象改为引用对象的目标。
3. 移除冗余参数
Scroll
的构造函数已经不再需要传入 title
和 tags
这两个参数了,因为这些信息可以通过 CatalogItem
引用来获取。所以,我们使用改变函数声明(124)把它们去掉。
Loader
类 (Java) - 最终版本
// ... (imports and Record class definition remain the same) ...public class Loader {private static Map<String, CatalogItem> catalogRepository = new HashMap<>();public static List<Scroll> loadScrolls(List<Record> records) {// Pre-load unique catalog items into the repositoryrecords.stream().map(r -> r.catalogData).collect(Collectors.toMap(cd -> cd.id,cd -> new CatalogItem(cd.id, cd.title, cd.tags),(existing, replacement) -> existing // In case of duplicate IDs, keep the existing one)).forEach((id, item) -> catalogRepository.putIfAbsent(id, item));// Final adjusted loading logicreturn records.stream().map(record -> new Scroll(record.id,LocalDate.parse(record.lastCleaned),record.catalogData.id,catalogRepository)) // Only pass necessary parameters.collect(Collectors.toList());}
}
Scroll
类 (Java) - 最终版本
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Set; // Still needed for hasTag if CatalogItem wasn't fully refactored yet, but conceptually its via CatalogItempublic class Scroll {private String id;private CatalogItem catalogItem;private LocalDate lastCleaned;// Final constructor with minimal parameterspublic Scroll(String id, LocalDate dateLastCleaned, String catalogID, Map<String, CatalogItem> catalogRepository) {this.id = id;this.catalogItem = catalogRepository.get(catalogID);if (this.catalogItem == null) {throw new IllegalArgumentException("Catalog item with ID " + catalogID + " not found.");}this.lastCleaned = dateLastCleaned;}public String getId() {return this.id;}// Forwarding methods for CatalogItem's public methodspublic String getTitle() {return catalogItem.getTitle();}public boolean hasTag(String arg) {return catalogItem.hasTag(arg);}// ... other forwarding methods for CatalogItem if any ...// Original Scroll methodspublic boolean needsCleaning(LocalDate targetDate) {long threshold = this.hasTag("revered") ? 700 : 1500;return daysSinceLastCleaning(targetDate) > threshold;}public long daysSinceLastCleaning(LocalDate targetDate) {return lastCleaned.until(targetDate, ChronoUnit.DAYS);}
}
至此,Scroll
类不再继承 CatalogItem
,而是通过委托复用其功能,并且 CatalogItem
被正确地作为引用对象处理,允许多个 Scroll
实例共享同一个目录项。这个重构过程提升了模型的清晰度,避免了“类型与实例名不符实”的建模错误,并为未来的扩展(例如,对 CatalogItem
的集中管理和更新)打下了更好的基础。
参考
《重构:改善既有代码的设计(第二版)》