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

第十二章:处理继承关系

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

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

具体展现:

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

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

第十一章:重构API

一、将查询函数和修改函数分离
动机:
如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很有价值的东西。因为我可以任意调用这个函数,也可以把调用动作搬到调用函数的其他地方。

这就对应一条好规则:任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离。

具体展现:

// 重构前
function getTotalOutstandingAndSendBill() {
    const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
    sendBill();
    return result;
}

// 重构后
function totalOutstanding() {
    const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {
    emailGateway.send(formatBill(customer));
}

二、函数参数化
动机:
如果发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。

具体展现:

// 重构前
function tenPercentRaise(aPerson) {
    aPerson.salary = aPerson.salary.multiply(1.1); 
}
function fivePercentRaise(aPerson) {
    aPerson.salary = aPerson.salary.multiply(1.05); 
}

// 重构后
function raise(aPerson, factor) {
    aPerson.salary = aPerson.salary.multiply(1 + factor); 
}

三、移除标记参数
动机:
“标记参数”是调用者用它来指示被调函数应该执行哪一部分逻辑。严格来说,只有参数值影响了函数内部的控制流,这才是标记参数。

移除标记参数不仅使代码更整洁,并且帮助开发工具更好的发挥作用。

具体展现:

// 重构前
function setDimension(name, value) {
    if (name === "height") {
        this._height = value;
        return;
    }
    if (name === "width") {
        this._width = value;
        return;
    }
}

// 重构后
function setHeight(value) { this._height = value; }
function setWidth (value) { this._width = value; }

四、保持对象完整
动机:
如果代码从一个记录结构中导出几个值,然后又把这几个值一起传递给一个函数,可以把整个记录传给这个函数,在函数体内部导出所需的值。

“传递整个记录”的方式能更好地应对变化:如果将来被调的函数需要从记录中导出更多的数据,我们就不用为此修改参数列表。传递整个记录也能缩短参数列表,让函数调用更容易看懂。

具体展现:

// 重构前
const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (aPlan.withinRange(low, high)) { 
    // 
}

// 重构后
if (aPlan.withinRange(aRoom.daysTempRange)) { 
    // 
}

五、以查询取代参数
动机:
参数列表应该尽量避免重复,并且参数列表越短就越容易理解。

如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。

有一种情况需要留意:如果正在处理的函数具有引用透明性,即:不论任何时候,只要传入相同的参数,该函数的行为永远一致。这样的函数即容易理解又容易测试,可以不去掉它的参数,而让它去访问一个可变的全局变量。

具体展现:

// 重构前
availableVacation(anEmployee, anEmployee.grade);

function availableVacation(anEmployee, grade) {
    //
}

// 重构后
availableVacation(anEmployee);

function availableVacation(anEmployee) {
    const grade = anEmployee.grade;
    // 
}

六、以参数取代查询
动机:
使用本重构的情况大多数源于我们想要改变代码的依赖关系——为了让目标不再依赖于某个元素,我把这个元素的指以参数的形式传递给该函数。

具体展现:

// 重构前
targetTemperature(aPlan) 

function targetTemperature(aPlan) {
    currentTemperature = thermostat.currentTemperature;
    // rest of function
}

// 重构后
targetTemperature(aPlan, thermostat.currentTemperature) 

function targetTemperature(aPlan, currentTemperature) {
    // rest of function
}

七、移除设值函数
动机:
如果为某个字段提供了设值函数,这就暗示这个字段可以被改变。如果不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数(同时将该字段声明为不可变)。

具体展现:

// 重构前
class Person {
    get name() { ... }
    set name(aString) { ... }
}

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

八、以工厂函数取代构造函数
动机:
需要新建一个对象时,客户端通常会调用构造函数。但与一般的函数相比,构造函数又常有一些局限性。

工厂函数不受这些限制。工厂函数的实现内部可以调用构造函数,但也可以换成别的方式实现。

具体展现:

// 重构前
leadEngineer = new Employee(document.leadEngineer, 'E');

// 重构后
leadEngineer = createEmployee(documet.leadEngineer);

九、以命令取代函数
动机:
将函数封装成自己的对象,有时也是一种有用的办法,这样的对象可以称为“命令对象”,或简称为“命令”。与普通函数相比,命令对象提供了更大的控制灵活性和更强的表达能力。除了函数调用本身,命令对象还可以支持附加的操作,例如撤销操作。

具体展现:

// 重构前
function score(candidate, medicalExam, scoringGuide) {
      let result = 0;
      let healthLevel = 0;
      // long body code
}

// 重构后
class Scorer {
    constructor(candidate, medicalExam, scoringGuide) {
        this._candidate = candidate;
        this._medicalExam = medicalExam;
        this._scoringGuide = scoringGuide;
    }
    execute() {
        this._result = 0;
        this._healthLevel= 0;
        // long body code
    }
}

十、以函数取代命令
动机:
命令对象为处理复杂计算提供了强大的机制,但这种强大是以复杂性为代价的。
如果一个函数不是太复杂,那么命令对象就显得不太需要了。

具体展现:

// 重构前
class ChargeCalculator {
    constructor(cutomer, usage) {
        this._cutomer = cutomer;
        this._usage = usage;
    }
    execute() {
        return this._cutomer.rate * this._usage;
    }
}

// 重构后
function charge(cutomer, usage) {
    return cutomer.rate * usage;
}