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

第十一章:重构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;
}