《重构》读后感(第十二章)

第十二章:处理继承关系

一、函数上移
动机:
避免重复代码是很重要的。

如果某个函数在各个子类中的函数体都相同,这就是显而易见的函数上移适合场景。

具体展现:

// 重构前
class Employee { ... }

class Salesman extends Employee {
    get name() { ... } 
}

class Enginner extends Employee {
    get name() { ... }
} 

// 重构后
class Employee { 
    get name() { ... } 
}

class Salesman extends Employee { ... } 
class Enginner extends Employee { ... }

二、字段上移
动机:
其实跟函数上移一样,只不过变成了重复的特性或字段等。

具体展现:

// 重构前
class Employee  { ... } // Java

class Salesman extends Employee {
    private String name;    
}

class Enginner extends Employee {
    private String name;    
} 

// 重构后
class Employee { 
    private String name;    
}

class Salesman extends Employee { ... } 
class Enginner extends Employee { ... }

三、构造函数本体上移
动机:
构造函数是很奇妙的东西。它们不是普通函数,使用它们比使用普通函数受到更多的限制。它们附加了特殊的规则,对一些做法与函数的调用次序有所限制。
如果构造过程过于复杂,可以考虑以工厂函数取代构造函数。

具体展现:

// 重构前
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;
    }
}

四、函数下移
动机:
如果超类中的某个函数只与一个(或少于几个)子类有关,那么最好将其从超类中移走,放到真正关心它的子类中去。

具体展现:

// 重构前
class Employee { 
    get quota() { ... } 
}

class Enginner extends Employee { ... }
class Salesman extends Employee { ... } 

// 重构后
class Employee { ... } 

class Enginner extends Employee { ... }
class Salesman extends Employee { 
    get quota() { ... } 
} 

五、字段下移
动机:
同上。

具体展现:

// 重构前
class Employee { //Java
    protected String quota;
}

class Enginner extends Employee { ... }
class Salesman extends Employee { ... } 

// 重构后
class Employee { ... } 
class Enginner extends Employee { ... }

class Salesman extends Employee { 
    protected String quota;
} 

六、以子类取代类型码
动机:
表现分类关系的第一种工具是类型码字段——根据具体的编程语言,可以分为枚举、符号、字符串或者数字。大多数时候,有这样的类型码就足够了。但也有些时候,我们可以更进一步,引入子类。继承有两个诱人之处:一是,可以用多态来处理条件逻辑。二是,有些字段或函数只对特定的类型码取值才有意义。

具体展现:

// 重构前
class createEmployee(name, type) { 
    return new Emplpyee(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);
    }
}

七、移除子类
动机:
随着软件的演化,子类所支持的变化可能会被搬移到别处,甚至完全去除,这时子类就失去了价值。子类存在着就有成本,阅读者需要花心思去理解它的用意,所以如果子类的用处太少,就不值得存在,此时最好的选择就是移除子类,将其替换为超类中的一个字段。

具体展现:

// 重构前
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() { ... }
}

九、折叠继承体系
动机:
在重构类继承体系时,我们经常把函数和字段上下移动。随着继承体系的演化,我们有时会发现一个类与其超类已经没有多大差别,不值得再作为独立的类存在。此时就可以把超类和子类合并起来。

具体展现:

// 重构前
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;
    }
}

十一、以委托取代超类
动机:
如果超类的一些函数对子类并不使用,就说明我们不应该通过继承来获得超类的功能。
合理的继承关系还有一个重要特征:子类的所有实例都应该是超类的实例,通过超类的接口来使用子类的实例应该完全不出问题。
其实这个重构方法跟上边那个“以委托取代子类”本质上是一样的,只不过取代的对象是超类中不适合子类的内容。

具体展现:

// 重构前
class List { ... }
class Stack extends List { ... }

// 重构后
class Stack { 
    constructor() {
        this._storage = new List();
    }
}
class List { ... }