反向重构:函数下移(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)将那些签名都修改为你想要在超类中使用的签名。
在超类中新建一个函数,将某一个待提升函数的代码复制到其中。
执行静态检查。
移除一个待提升的子类函数。
测试。
逐一移除待提升的子类函数,直到只剩下超类中的函数为止。
反向重构:字段下移(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权限便已足够)。
移除子类中的字段。
测试。
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)。
反向重构:函数上移(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),只留些共用的行为在超类。
反向重构:字段上移(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;
}
如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要该字段的子类中。
包含旧重构:以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)。
曾用名:以字段取代子类(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;}
}
子类很有用,它们为数据结构的多样和行为的多态提供支持,它们是针对差异编程的好工具。但随着软件的演化,子类所支持的变化可能会被搬移到别处,甚至完全去除,这时子类就失去了价值。有时添加子类是为了应对未来的功能,结果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要了。
子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用处太少,就不值得存在了。此时,最好的选择就是移除子类,将其替换为超类中的一个字段。
本重构手法常用于一次移除多个子类,此时需要先把这些子类都封装起来(添加工厂函数、搬移类型检查),然后再逐个将它们折叠到超类中。
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)。这两种方案之间的选择,其实就是继承和委托之间的选择,总之目的都是把重复的行为收拢一处。
class Employee {/*...*/}
class Salesman extends Employee {/*...*/}
class Employee {/*...*/}
在重构类继承体系时,如果会发现一个类与其超类已经没多大差别,可以把超类和子类合并起来。
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
}
}
如果一个对象的行为有明显的类别之分,继承是很自然的表达方式。可以把共用的数据和行为放在超类中,每个子类根据需要覆写部分特性。在面向对象语言中,继承很容易实现,因此也是程序员熟悉的机制。
但继承也有其短板。最明显的是,继承这张牌只能打一次。导致行为不同的原因可能有多种,但继承只能用于处理一个方向上的变化。更大的问题在于,继承给类之间引入了非常紧密的关系。在超类上做任何修改,都很可能破坏子类。
这两个问题用委托都能解决。对于不同的变化原因,可以委托给不同的类。委托是对象之间常规的关系。与继承关系相比,使用委托关系时接口更清晰、耦合更少。
有一条流行的原则:“对象组合优于类继承”(“组合”跟“委托”是同一回事)。
曾用名:以委托取代继承(Replace Inheritance with Delegate)
class List {/*...*/}
class Stack extends List {/*...*/}
class Stack {
constructor() {
this._storage = new List();
}
}
class List {/*...*/}
在面向对象程序中,通过继承来复用现有功能,是一种既强大又便捷的手段。只要继承一个已有的类,覆写一些功能,再添加一些功能,就能达成目的。但继承也有可能造成困扰和混乱。
如果超类的一些函数对子类并不适用,就说明不应该通过继承来获得超类的功能。
合理的继承关系有一个重要特征:子类的所有实例都应该是超类的实例,通过超类的接口来使用子类的实例应该完全不出问题。