《重构》读后感(第八章)

第八章:搬移特性。

本章主要讲的重构手法是,在不同的上下文之间搬移元素。

一、搬移函数
动机:
任何函数都需要具备上下文环境才能存活。对一个面向对象的程序而言,类作为最主要的模块化手段,其本身就能充当函数的上下文;通过嵌套的方式,外层函数也能为内层函数提供一个上下文。

搬移函数最直接的动因是:频繁引用其他上下文中的元素,而对自身上下文中的元素却关心甚少。

是否需要搬移函数常常不易抉择,最好的做法就是先把函数安置到某一个上下文中去,这样我们就能发现它们是否契合,如果不太合适我们可以再把函数搬移到别的地方。

具体展现:

// 重构前
class Account{
    get overdraftCharge() { ... }
}

// 重构后
class AccountType {
    get overdraftCharge() { ... }
}

二、搬移字段
动机:
往往数据结构才是一个健壮程序的根基。

搬移数据,原因包括发现每当调用某个函数时,除了传入一个记录参数,还总是需要同时传入另一条记录的某个字段一起作为参数。总是一同出现、一同作为函数参数传递的数据,最好是调整到同一记录中,以体现它们之间的联系。修改的难度也是一个原因,如果修改一条记录时,总是需要同时改动另一条记录,那么说明很可能有字段放错了位置。或者如果更新一个字段时,需要同时在多个结构中做出修改,那也是一个征兆,表明该字段需要被搬移到一个集中的地方,这样每次只需要修改一处地方。

具体展现:

// 重构前
class Customer {
    get plan() { return this._plan; }
    get discountRate() { return this._discountRate; }
}

// 重构后
class Customer {
    get plan() { return this._plan; }
    get discountRate() { return this.plan.discountRate; }
}

三、搬移语句到函数
动机:
要维护代码库的健康发展,需要遵守几条黄金守则,其中最重要的一条当属”消除重复”。

如果发现调用某个函数时,总有一些相同的代码也需要每次执行,那么会将此段代码合并到函数里。如果将来代码对不同的调用者需有不同的行为,那时再通过搬移语句到调用者将它搬移出来也很简单。

具体展现:

// 重构前
result.push('<p>title: ${person.photo.title}</p>');
result.concat(photoData(person.photo));

function photoData(aPhoto) {
    return [
        '<p>location: ${aPhoto.location}</p>',
        '<p>date: ${aPhoto.date.toDateString()}</p>',
    ];
}

// 重构后
result.push(photoData(person.photo));

function photoData(aPhoto) {
    return [
        '<p>title: ${aPhoto.title}</p>'
        '<p>location: ${aPhoto.location}</p>',
        '<p>date: ${aPhoto.date.toDateString()}</p>',
    ];
}

四、搬移语句到调用者
动机:
随着系统能力的演进,原先设定的抽象边界总是悄无声息地发生偏移。对于函数来说,这样的边界偏移意味着曾经视为一个整体、一个单元的行为,如今可能已经分化出两个甚至是多个不同的关注点。

函数边界发生偏移的一个征兆是,以往多个地方公用的行为,如今需要在某些调用点面前表现出不同的行为。因此,我们需要把表现不同的行为从函数里挪出,并搬移到其调用处。

这个重构手法比较适合处理边界仅有些许便宜的场景,但有时调用点和调用者之间的边界已经相去甚远,此时便只能重新进行设计了。若果真如此,最好的办法就是先用内联函数合并双方的内容。调整语句的顺序,再提炼出新的函数,以形成更合适的边界。

具体展现:

// 重构前
emitPhotoData(outStream, person.photo); 

function emitPhotoData(outStream, photo) {
    outStream.write('<p>title: ${aPhoto.title}</p>\n');
    outStream.write('<p>location: ${aPhoto.location}</p>\n');
}

// 重构后
emitPhotoData(outStream, person.photo); 
outStream.write('<p>location: ${aPhoto.location}</p>\n');

function emitPhotoData(outStream, photo) {
    outStream.write('<p>title: ${aPhoto.title}</p>\n');
}

五、以函数调用取代内联代码
动机:
善用函数可以帮助我们将相关的行为打包起来,这对于提升代码的表达力大有裨益(当然需要一个好的函数名)。如果我见一些内联代码,它们做的事情仅仅是已有函数的重复,我们可以用一个函数调用取代内联代码。

具体展现:

// 重构前
let appliesToMass = false;
for (const s of states) {
    if (s == "MA") appliesToMass = true;
}

// 重构后
appliesToMass = states.includes("MA");

六、移动语句
动机:
让存在关联的东西一起出现,可以是代码更容易理解。如果有几行代码取用了同一个数据结构,那么最好是让他们在一起出现,而不是夹杂在取用其他数据结构的代码中间。

把相关代码搜集到一处,往往是另一项重构(通常是提炼函数)开始之前的准备工作。

具体展现:

// 重构前
const pricingPlan = retrievePricingPlan();
const order = retrieveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;

// 重构后
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retrieveOrder();
let charge;

七、拆分循环
动机:
你常常见到一个循环内做两三件事情,原因是这样做可以只循环一次。但带来的问题是,每当需要修改循环时,就得同时理解这两件事情。

拆分循环还能让每个循环更容易使用。如果一个循环只计算一个值,那么它直接返回该值即可。一般拆分循环后,紧接着将拆分得到的循环应用提炼函数。

你可能拆分成多个循环会使性能受到影响。但还是之前提到的:先进行重构,然后再进行性能优化。如果重构之后该循环确实成了性能的瓶颈,再把拆开的循环合到一起也很容易。但实际情况是,即使处理的列表数据更多一些,循环本身也很少成为性能瓶颈,更何况拆分出循环来通常还使一些更强大的性能优化成为可能。(我感觉这个需要看情况自行判断是否需要如此做)

具体展现:

// 重构前
let averageAge = 0;
let totalSalary = 0;
for (const p of people) {
    averageAge += p.age;
    totalSalary += p.salary;
}
averageAge = averageAge / people.length;

// 重构后
let totalSalary = 0;
for (const p of people) {
    totalSalary += p.salary;
}

let averageAge = 0;
for (const p of people) {
    averageAge += p.age;
}
averageAge = averageAge / people.length;

八、以管道取代循环
动机:
以前迭代一组集合时需要使用循环。现在越来越多的编程语言(不包括iOS)都提供了更好的语言结构来处理迭代过程,这种结构就叫做:集合管道(collection pipeline),集合管道允许使用一组运算来描述集合的迭代过程,其中每种运算接收的入参和返回值都是一个集合。

具体展现:

// 重构前
const names = [];
for (const i of input) {
    if (i.job == "programmer") {
        names.push(i.name);
    }
}

// 重构后
const names = input
    .filter(i => i.job === "programmer")
    .map(i => i.name)
;

九、 移除死代码
动机:
无用代码可能对性能、内存不会带来影响,但对于尝试阅读、理解软件运作原理时,无用代码确实会带来很多额外的思维负担。

一旦代码不再被使用,就应该立马删除它。我们可能经常想着以后可能会用到,不用担心这个,我们可以从版本控制系统里找回它的。

具体展现:

// 重构前
if (false) {
    doSomethingThatUsedToMatter();
}

// 重构后
移除该代码