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

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

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

第九章:重新组织数据。

一、拆分变量
动机:
变量有各种不同的用途,其中某些用途会很自然地导致临时变量被多次赋值。”循环变量”和”结果收集变量”就是两个例子:循环变量会随循环的每次运行而改变;结果收集变量负责将”通过整个函数的运算”而构成的某个值收集起来。

除了以上两种情况,还有很多变量用于保存一段冗长代码的运算结果,以便稍后使用。这种变量应该只被赋值一次。如果被赋值超过一次,就意味着它们在函数中承担了一个以上的责任。如果变量承担多个责任,就应该被替换(分解)为多个变量,每个变量只承担一个责任。同一个变量承担两件不同的事情,会令代码阅读者糊涂。

具体展现:

// 重构前
let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);

// 重构后
const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width;
console.log(area);

二、字段改名
动机:
命名很重要,这个不需要再过多的重复了。

具体展现:

// 重构前
class Organization {
    get name() { ... }
}

// 重构后
class Organization {
    get title() { ... }
}

三、以查询取代派生变量
动机:
可变数据是软件中最大的错误源头之一。

有些变量其实可以很容易地随时计算出来。如果能去掉这些变量,也能消除可变性。计算常能更清晰地表达数据的含义,而且也避免了”源数据修改时忘了更新派生变量”的错误。

具体展现:

// 重构前
get discountedTotal() { return this._discountedTotal; }
set discount(aNamber) {
    const old = this._discount;
    this._discount = aNamber;
    this._discountedTotal += old - aNamber;
}

// 重构后
get discountedTotal() { return this._baseTotal - this._discount; }
set discount(aNamber) { this._discount = aNamber; }

四、将引用对象改为值对象
动机:
在把一个对象(或数据结构)嵌入另一个对象时,位于内部的对象可以被视为引用对象,也可以被视为值对象。其最明显的差别在于:视为引用对象的话,在更新其属性时,会保留原对象不动,更新内部对象的属性。如果视为值对象,就可以替换整个内部对象,把内部对象的类也变成值对象。

值对象是不可变的。因此可以放心地把不可变的数据值传给程序的其他部分,而不必担心对象中包装的数据被偷偷修改。可以在程序各处复制值对象,而不必操心维护内存链接。

如果想在几个对象之间共享一个对象,以便几个对象都能看见对共享对象的修改,那么这个共享的对象就应该是引用。

具体展现:

// 重构前
class Product {
    applyDiscount(arg) { this._price.amount -= arg; }
}

// 重构后
class Product {
    applyDiscount(arg) {
        this._price = new Money(this._price.amount - arg, this._price.currency);
    }
}

五、将值对象改为引用对象
动机:
正如上面的重构方法所知,把数据作为值对象和引用对象都可以。但需要根据具体需求而定。

如果共享的数据需要更新,将其复制多份的做法就会遇到巨大的问题。对于这种情况,可以考虑将多分数据副本变成单一的引用,这样对一处数据的修改就会立即反映到所有引用的数据中。

如果要将值对象转换成引用对象,可以创建一个仓库对象,仓库对象存储所有唯一的值,在需要使用到值的构造函数中将仓库引用进来就可以了。

具体展现:

// 重构前
let customer = new Customer(customerData);

// 重构后
let customer = customerRepository.get(customerData.id);

第十章:简化条件逻辑

程序的大部分威力来自条件逻辑,但很不幸,程序的复杂程度也大多来自条件逻辑。

一、分解条件表达式
动机:
程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。

和任何大块头代码一样,对于复杂逻辑的函数,我们可以将它分解为多个独立的函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函数,从而更清楚地表达自己的意图。对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。

具体展现:

// 重构前
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) {
    charge = quantity * plan.summerRate;
} else {
    charge = quantity * plan.regularRate + plan.regularServiceCharge;
}

// 重构后
if (summer()) {
    charge = summerCharge();
} else {
    charge = regularCharge();
}

二、合并条件表达式
动机:
如果一串检查条件各不相同,但最终行为一致,就应该使用”逻辑或”和”逻辑与”将它们合并为一个条件表达式。

具体展现:

// 重构前
if (anExployee.seniority < 2) return 0;
if (anExployee.monthsDisabled > 12) return 0;
if (anExployee.isPartTime) return 0;

// 重构后
if (isNotEligibleForDisability()) return 0;

function isNotEligibleForDisability() {
    return ((anExployee.seniority < 2) 
            || (anExployee.monthsDisabled > 12) 
            || (anExployee.isPartTime));
}

三、以卫语句取代嵌套条件表达式
动机:
条件表达式通常有两种风格。第一种风格是:两个条件分支都属于正常行为。第二种风格是:只有一个条件分支是正常行为,另一个分支则是异常的情况。

如果两条分支都是正常行为,就应该使用形如if … else …的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时从函数中返回。这样的单独检查常常被称为”卫语句”(guard clauses)。

卫语句的精髓就是:给某一条分支以特别的重视。卫语句告诉读者:“这种情况不是本函数的核心逻辑所关心的,如果真的发生了,请做一些必要的整理工作,然后退出”。

具体展现:

// 重构前
function getPayAmount() {
    let result;
    if (isDead) {
        result = deadAmount();
    } else {
        if (isSeparated) {
            result = separatedAmount();
        } else {
            if (isRetired) {
                result = retiredAmount();
            } else {
                result = normalPayAmount();
            }
        }
    }
    return result;
}

// 重构后
function getPayAmount() {
    if (isDead) return deadAmount();
    if (isSeparated) return separatedAmount();
    if (isRetired) return retiredAmount();
    return normalPayAmount();
}

四、 以多态取代条件表达式
动机:
一种常见场景:如果有好几个函数都有基于类型代码的switch语句,我们就可以针对每种分支逻辑创建一个类,用多态来承载各个类型特有的行为,从而去除重复的分支逻辑。

另一种情况:基础逻辑可能是最常用的,也可能是最简单的。可以把基础逻辑放进超类,这样就首先可以理解在和部分逻辑,暂时不管各种变体,然后可以把每种变体逻辑单独放进一个子类,其中的代码着重强调与基础逻辑的差异。

具体展现:

// 重构前
switch (brid.type) {
    case 'EuropeanSwallow':
        return 'average';
    case 'AfricanSwallow':
        return (bird.numberOfCoconuts > 2) ? 'tired' : 'average';
    case 'NorwegianBlueParrot':
        return (bird.voltage > 100) ? 'scorched' : 'beautiful';
    default:
        return 'unknown';
}

// 重构后
class EuropeanSwallow {
  get plumage() {
    return 'average';
  }
}

class AfricanSwallow {
  get plumage() {
    return (this.numberOfCoconuts > 2) ? 'tired' : 'average';
  }
}

class NorwegianBlueParrot {
  get plumage() {
    return (this.voltage > 100) ? 'scorched' : 'beautiful';
  }
}

五、引入特例
动机:
有一种常见的重复代码情况:一个数据结构的使用者都在检查某个特殊的值,并且当这个特殊值出现时所做的处理也都相同。如果发现代码库中有多处以同样方式应对同一个特殊值,应该把这个处理逻辑收拢在一起。处理这种情况的一个好办法就是使用“特例”模式:创建一个特例元素,用以表达对这种特例的共用行为的处理。

一个通常需要特殊处理的值就是null,这也是这个模式常被叫做”Null对象”(Null Object)模式的原因,Null对象是特例的一种特例。

具体展现:

// 重构前
if (aCustomer === "unknown") customerName = "occupant";

// 重构后
class UnknownCustomer {
    get name() { return "occupant"; }
}

六、引入断言
动机:
常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行。这样的假设通常并没有在代码中明确的表现出了,我们可以使用断言来明确标明这些假设。

断言是一个条件表达式,应该总是为真。如果它失败,表示程序员犯了错误,断言的失败不应被系统任何地方捕获,整个程序的行为在有没有断言出现的时候都应该完全一样。

断言是一种很有价值的交流形式——它告诉阅读者,程序执行到这一点时,对当前状态做出了何种假设。另外断言对调试也很有帮助。

另外,不要滥用断言。不要使用断言来检查“我认为应该为真”的条件,应该来检查“必须为真”的条件。

具体展现:

// 重构前
if (this.discountRate) {
    base = base - (this.discountRate * base);
}

// 重构后
assert(this.discountRate >= 0);
if (this.discountRate) {
    base = base - (this.discountRate * base);
}