函数上移(Pull Up Method)
反向重构:函数下移(Push Down Method)
class Employee {/*...*/}
class Salesman extends Employee {
get name() {/*...*/}
}
class Engineer extends Employee {
get name() {/*...*/}
}
class Employee {
get name() {/*...*/}
}
class Salesman extends Employee {/*...*/}
class Engineer extends Employee {/*...*/}
动机
避免重复代码是很重要的。重复的两个函数现在也许能够正常工作,但假以时日却只会成为滋生bug的温床。无论何时,只要系统内出现重复,你就会面临“修改其中一个却未能修改另一个”的风险。
如果某个函数在各个子类中的函数体都相同(它们很可能是通过复制粘贴得到的),这就是最显而易见的函数上移
适用场合。
函数上移过程中最麻烦的一点就是,被提升的函数可能会引用只出现于子类而不出现于超类的特性。此时,就得用字段上移(353)和函数上移先将这些特性(类或者函数)提升到超类。
做法
-
检查待提升函数,确定它们是完全一致的。
如果它们做了相同的事情,但函数体并不完全一致,那就先对它们进行重构,直到其函数体完全一致。
-
检查函数体内引用的所有函数调用和字段都能从超类中调用到。
-
如果待提升函数的签名不同,使用改变函数声明(124)将那些签名都修改为你想要在超类中使用的签名。
-
在超类中新建一个函数,将某一个待提升函数的代码复制到其中。
-
执行静态检查。
-
移除一个待提升的子类函数。
-
测试。
-
逐一移除待提升的子类函数,直到只剩下超类中的函数为止。
字段上移(Pull Up Field)
反向重构:字段下移(Push Down Field)
class Employee {/*...*/} // Java
class Salesman extends Employee {
private String name;
}
class Engineer extends Employee {
private String name;
}
class Employee {
protected String name;
}
class Salesman extends Employee {/*...*/}
class Engineer extends Employee {/*...*/}
动机
如果各子类是分别开发的,或者是在重构过程中组合起来的,常常会发现它们拥有重复特性,特别是字段更容易重复。
本项重构可从两方面减少重复:首先它去除了重复的数据声明;其次可以将使用该字段的行为从子类移至超类,从而去除重复的行为。
做法
-
针对待提升之字段,检查它们的所有使用点,确认它们以同样的方式被使用。
-
如果这些字段的名称不同,先使用变量改名(137)为它们取个相同的名字。
-
在超类中新建一个字段。
新字段需要对所有子类可见(在大多数语言中protected权限便已足够)。
-
移除子类中的字段。
-
测试。
构造函数本体上移(Pull Up Constructor Body)
class Party {/*...*/}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super();
this._id = id;
this._name = name;
this._monthlyCost = monthlyCost;
}
}
class Party {
constructor(name){
this._name = name;
}
}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super(name);
this._id = id;
this._monthlyCost = monthlyCost;
}
}
动机
构造函数是很奇妙的东西。它们不是普通函数,使用它们比使用普通函数受到更多的限制。
如果看见各个子类中的函数有共同行为,可以使用提炼函数(106)将它们提炼到一个独立函数中,然后使用函数上移(350)将这个函数提升至超类。
如果重构过程过于复杂,我会考虑转而使用以工厂函数取代构造函数(334)。
做法
- 如果超类还不存在构造函数,首先为其定义一个。确保让子类调用超类的构造函数。
- 使用移动语句(223)将子类中构造函数中的公共语句移动到超类的构造函数调用语句之后。
- 逐一移除子类间的公共代码,将其提升至超类构造函数中。对于公共代码中引用到的变量,将其作为参数传递给超类的构造函数。
- 测试。
- 如果存在无法简单提升至超类的公共代码,先应用提炼函数(106),再利用函数上移(350)提升之。
函数下移(Push Down Method)
反向重构:函数上移(Pull up Method)
class Employee {
get quota {/*...*/}
}
class Engineer extends Employee {/*...*/}
class Salesman extends Employee {/*...*/}
class Employee {/*...*/}
class Engineer extends Employee {/*...*/}
class Salesman extends Employee {
get quota {/*...*/}
}
动机
如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从超类中挪走,放到真正关心它的子类中去。这项重构手法只有在超类明确知道哪些子类需要这个函数时适用。如果超类不知晓这个信息,那就得用以多态取代条件表达式(272),只留些共用的行为在超类。
做法
- 将超类中的函数本体复制到每一个需要此函数的子类中。
- 删除超类中的函数。
- 测试。
- 将该函数从所有不需要它的那些子类中删除。
- 测试。
字段下移(Push Down Field)
反向重构:字段上移(Pull Up Field)
class Employee { // Java
private String quota;
}
class Engineer extends Employee {/*...*/}
class Salesman extends Employee {/*...*/}
class Employee {/*...*/}
class Engineer extends Employee {/*...*/}
class Salesman extends Employee {
protected String quota;
}
动机
如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要该字段的子类中。
做法
- 在所有需要该字段的子类中声明该字段。
- 将该字段从超类中移除。
- 测试。
- 将该字段从所有不需要它的那些子类中删掉。
- 测试。
以子类取代类型码(Replace Type Code with Subclasses)
包含旧重构:以State/Strategy取代类型码(Replace Type Code with State/Strategy)
包含旧重构:提炼子类(Extract Subclass)
反向重构:移除子类(Remove Subclass)
function createEmployee(name, type) {
return new Employee(name, type);
}
function createEmployee(name, type) {
switch (type) {
case "engineer": return new Engineer(name);
case "salesman": return new Salesman(name);
case "manager": return new Manager (name);
}
}
动机
软件系统经常需要表现“相似但又不同的东西”,比如员工可以按职位分类(工程师、经理、销售)。表现分类关系的第一种工具是类型码字段——根据具体的编程语言,可能实现为枚举、符号、字符串或者数字。
大多数时候,有这样的类型码就够了。也可以更进一步,引入子类:可以用多态来处理条件逻辑。如果有几个函数都在根据类型码的取值采取不同的行为,多态就显得特别有用。引入子类之后,可以用以多态取代条件表达式(272)来处理这些函数。
另外,有些字段或函数只对特定的类型码取值才有意义,例如“销售目标”只对“销售”这类员工才有意义。此时可以创建子类,然后用字段下移(361)把这样的字段放到合适的子类中去。
在使用以子类取代类型码时,需要考虑一个问题:应该直接处理携带类型码的这个类,还是应该处理类型码本身
呢?如果子类的类别是可变的,那么也不能使用直接继承的方案。可以运用以对象取代基本类型(174)把类型码包装成“父级”类,然后对其使用以子类取代类型码(362)。
做法
- 自封装类型码字段。
- 任选一个类型码取值,为其创建一个子类。覆盖类型码类的取值函数,令其返回该类型码的字面量值。
- 创建一个选择器逻辑,把类型码参数映射到新的子类。
- 测试。
- 针对每个类型码取值,重复上述”创建子类,添加选择器逻辑“的过程。每次修改后执行测试。
- 去除类型码字段。
- 测试。
- 使用函数下移(359)和以多态取代条件表达式(272)处理原本访问了类型码的函数。全部处理完后,就可以移除类型码的访问函数。
移除子类(Remove Subclass)
曾用名:以字段取代子类(Replace Subclass with Fields)
反向重构:以子类取代类型码(362)
class Person {
get genderCode() {return "X";}
}
class Male extends Person {
get genderCode() {return "M";}
}
class Female extends Person {
get genderCode() {return "F";}
}
class Person {
get genderCode() {return this._genderCode;}
}
动机
子类很有用,它们为数据结构的多样和行为的多态提供支持,它们是针对差异编程的好工具。但随着软件的演化,子类所支持的变化可能会被搬移到别处,甚至完全去除,这时子类就失去了价值。有时添加子类是为了应对未来的功能,结果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要了。
子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用处太少,就不值得存在了。此时,最好的选择就是移除子类,将其替换为超类中的一个字段。
做法
- 使用以工厂函数取代构造函数(334),把子类的构造函数包装到超类的工厂函数中。
- 如果有任何代码检查子类的类型,先用提炼函数(106)把类型检查逻辑包装起来,然后用搬移函数(198)将其搬到超类。每次修改后执行测试。
- 新建一个字段,用于代表子类的类型。
- 将原本针对子类的类型做判断的函数改为使用新建的类型字段。
- 删除子类。
- 测试。
本重构手法常用于一次移除多个子类,此时需要先把这些子类都封装起来(添加工厂函数、搬移类型检查),然后再逐个将它们折叠到超类中。
提炼超类(Extract Superclass)
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)。这两种方案之间的选择,其实就是继承和委托之间的选择,总之目的都是把重复的行为收拢一处。
做法
- 为原本的类新建一个空白的超类。
- 测试。
- 使用构造函数本体上移(355)、函数上移(350)和字段上移(353)手法,逐一将子类的共同元素上移到超类。
- 检查留在子类中的函数,看它们是否还有共同的成分。如果有,可以先用提炼函数(106)将其提炼出来,再用函数上移(350)搬到超类。
- 检查所有使用原本的类的客户端代码,考虑将其调整为用超类的接口。
折叠继承关系(Collapse Hierarchy)
class Employee {/*...*/}
class Salesman extends Employee {/*...*/}
class Employee {/*...*/}
动机
在重构类继承体系时,如果会发现一个类与其超类已经没多大差别,可以把超类和子类合并起来。
做法
- 选择想移除的类:是超类还是子类?
- 使用字段上移(353)、字段下移(361)、函数上移(350)和函数下移(359),把所有元素都移到同一个类中。
- 调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类。
- 移除我们的目标;此时它应该已经成为一个空类。
- 测试。
以委托取代子类(Replace Subclass with Delegate)
class Order {
get daysToShip() {
return this._warehouse.daysToShip;
}
}
class PriorityOrder extends Order {
get daysToShip() {
return this._priorityPlan.daysToShip;
}
}
class Order {
get daysToShip() {
return (this._priorityDelegate)
? this._priorityDelegate.daysToShip
: this._warehouse.daysToShip;
}
}
class PriorityOrderDelegate {
get daysToShip() {
return this._priorityPlan.daysToShip
}
}
动机
如果一个对象的行为有明显的类别之分,继承是很自然的表达方式。可以把共用的数据和行为放在超类中,每个子类根据需要覆写部分特性。在面向对象语言中,继承很容易实现,因此也是程序员熟悉的机制。
但继承也有其短板。最明显的是,继承这张牌只能打一次。导致行为不同的原因可能有多种,但继承只能用于处理一个方向上的变化。更大的问题在于,继承给类之间引入了非常紧密的关系。在超类上做任何修改,都很可能破坏子类。
这两个问题用委托都能解决。对于不同的变化原因,可以委托给不同的类。委托是对象之间常规的关系。与继承关系相比,使用委托关系时接口更清晰、耦合更少。
有一条流行的原则:“对象组合优于类继承”(“组合”跟“委托”是同一回事)。
做法
- 如果构造函数有多个调用者,首先用以工厂函数取代构造函数(334)把构造函数包装起来。
- 创建一个空的委托类,这个类的构造函数应该接受所有子类特有的数据项,并且经常以参数的形式接受一个指回超类的引用。
- 在超类中添加一个字段,用于安放委托对象。
- 修改子类的创建逻辑,使其初始化上述委托字段,放入一个委托对象的实例。
- 选择一个子类中的函数,将其移入委托类。
- 使用搬移函数(198)手法搬移上述函数,不要删除源类中的委托代码。
- 如果被搬移的源函数还在子类之外被调用了,就把留在源类中的委托代码从子类移到超类,并在委托代码之前加上卫语句,检查委托对象存在。如果子类之外已经没有其他调用者,就用移除死代码(237)去掉已经没人使用的委托代码。
- 测试。
- 重复上述过程,直到子类中所有函数都搬到委托类。
- 找到所有调用子类构造函数的地方,逐一将其改为使用超类的构造函数。
- 测试。
- 运用移除死代码(237)去掉子类。
以委托取代超类(Replace Superclass with Delegate)
曾用名:以委托取代继承(Replace Inheritance with Delegate)
class List {/*...*/}
class Stack extends List {/*...*/}
class Stack {
constructor() {
this._storage = new List();
}
}
class List {/*...*/}
动机
在面向对象程序中,通过继承来复用现有功能,是一种既强大又便捷的手段。只要继承一个已有的类,覆写一些功能,再添加一些功能,就能达成目的。但继承也有可能造成困扰和混乱。
如果超类的一些函数对子类并不适用,就说明不应该通过继承来获得超类的功能。
合理的继承关系有一个重要特征:子类的所有实例都应该是超类的实例,通过超类的接口来使用子类的实例应该完全不出问题。
做法
- 在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的新实例。
- 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发给委托引用。每转发一块完整逻辑,都要执行测试。
- 当所有超类函数都被转发函数覆写后,就可以去掉继承关系。