《重构》读书笔记(一)
一.说明
1.《重构》读书笔记指的是对fowler先生《重构》第二版的阅读观感(以下称重构2),这个版本或许像译者所说的fowler先生想要传达的理念是:
千里之行积于跬步,越是面对复杂多变的外部环境,越是要做好基本功、迈出扎实步。
2. 译者认为重构2的重构原则是: " 旧的不变,新的创建,一步切换,旧的再见。"
3.《重构》传达的是一种工匠精神。我喜欢的译者观点是:一个对匠艺上心的专业人士,日积月累对过程与方式的重视,是能有所成就的。
二.第一章
1.何为重构?
重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减小整理过程中引入错误的概率。本质上说,重构就是在代码写好之后改进它的设计。
2.为何重构?
在软件开发的大部分历史时期,大部分人相信应该先设计而后编码:首先得有一个良好的设计,然后才能开始编码。
但是,随着时间流逝,人们不断修改代码,于是根据原先设计所得的系统,整体结构逐渐衰弱。代码质量慢慢沉沦,编码工作从严谨的工程堕落为胡砍乱劈的随性行为。
plays.json: { "hamlet": {"name": "Hamlet", "type": "tragedy"}, "as-like": {"name": "As You Like It", "type": "comedy"}, "othello": {"name": "Othello", "type": "tragedy"}} invoices.json: [ { "customer": "BigCo", "performances": [ { "playID": "hamlet", "audience": 55 }, { "playID": "as-like", "audience": 35 }, { "playID": "othello", "audience": 40 } ] }]; 账单函数: function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`;
const format = new Intl.NumberFormat("en-US",
{ style: "currency", currency: "USD",
minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { const play = plays[perf.playID]; let thisAmount = 0; switch (play.type) { case "tragedy": thisAmount = 40000; if (perf.audience > 30) { thisAmount += 1000 * (perf.audience - 30); } break; case "comedy": thisAmount = 30000; if (perf.audience > 20) { thisAmount += 10000 + 500 * (perf.audience - 20); } thisAmount += 300 * perf.audience; break; default: throw new Error(`unknown type: ${play.type}`); }
// add volume credits volumeCredits += Math.max(perf.audience - 30, 0); // add extra credit for every ten comedy attendees if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5); // print line for this order result += ` ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`; totalAmount += thisAmount;
} result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result;
} 用上面的数据文件(invoices.json和plays.json)作为测试输入,运行这段代码,会得到如下输出: Statement for BigCo Hamlet: $650.00 (55 seats) As You Like It: $580.00 (35 seats) Othello: $500.00 (40 seats) Amount owed is $1,730.00 You earned 47 credits
这样的设计:代码组织不太清晰,但还在可以忍受的限度内。但如果这段代码身处一个更大规模--也许几百行--的程序中,把所有代码放在一个函数里就很难理解了。
虽然结构不清晰却是可以使用,对编译器来说没有影响,但是,当我需要修改的时候,差劲的系统就很难找到修改点。就很可能犯错误,引入bug。
因此如果要修改一个几百行的程序,我们会期望它有良好的结构,并且已经分解成一系列函数和其他程序要素,这能帮我们更易于清楚的了解这段代码在做什么。但如果程序杂乱无章,先为它整理出结构来,再做需要的修改,通常更加简单。
* 如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,
使其比较容易添加该特性,然后再添加该特性。
试想修改:1.以HTML格式输出详单;2.戏剧分类规则和计费规则的变化;这些修改可能在几个月后还会出现变化。
如果不重构,函数则会变得越来越杂乱,需求的变化使重构变得必要!!!
重构过程:
A.重构第一步:得确保即将修改的代码拥有一组可靠的测试。使测试能自我检验至关重要,否则就得耗费大把时间来回比对,这会降低开发速度。
* 重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验能力。
B.分解statement函数:
[原文]:每当看到这样长长的函数,我便下意识地想从整个函数中分离出不同的关注点。第一个引起我注意的就是中间那段switch语句。
switch (play.type) { case "tragedy": thisAmount = 40000;
if (perf.audience > 30) {
thisAmount += 1000 * (perf.audience - 30);
}
break;
case "comedy":
thisAmount = 30000;
if (perf.audience > 20) {
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
break;
default:
throw new Error(`unknown type: ${play.type}`);
}
function amountFor(perf, play) { let thisAmount = 0; switch (play.type) { case "tragedy": thisAmount = 40000; if (perf.audience > 30) { thisAmount += 1000 * (perf.audience - 30); } break; case "comedy": thisAmount = 30000; if (perf.audience > 20) { thisAmount += 10000 + 500 * (perf.audience - 20); } thisAmount += 300 * perf.audience; break; default: throw new Error(`unknown type: ${play.type}`); } return thisAmount;}
做完这个改动后,我会马上编译并执行一遍测试,看看有无破坏了其他东西。无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。小步修改,以及它带来的频繁反馈,正是防止混乱的关键。
重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它
傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。
C.移除play变量
当分解一个长函数时,我喜欢将play这样的变量移除掉,因为它们创建了很多具有局部作用域的临时变量,这会使提炼函数更加复杂。
编译、测试、提交。完成变量内联后,我可以对amountFor函数应用改变函数声明(124),移除play参数。我会分两步走。首先在amountFor函数内部使用新提炼的函数。
然后再一次编译、测试、提交。
这次重构可能在一些程序员心中敲响警钟:重构前,查找play变量的代码在每次循环中只执行了1次,而重构后却执行了3次。我会在后面探讨重构与性能之间的关系,但现在,我认为这次改动还不太可能对性能有严重影响,即便真的有所影响,后续再对一段结构良好的代码进行性能调优,也容易得多。
D.提炼计算关总量积分的逻辑
E.移除format变量
正如我上面所指出的,临时变量往往会带来麻烦。它们只在对其进行处理的代码块中有用,因此临时变量实质上是鼓励你写长而复杂的函数。因此,下一步我要替换掉一些临时变量,而最简单的莫过于从format变量入手。这是典型的“将函数赋值给临时变量”的场景,我更愿意将其替换为一个明确声明的函数。
format函数更名
F.移除观众量积分总和
把与更新volumeCredits变量相关的代码都集中到一起,有利于以查询取代临时变量(178)手法的施展。第一步同样是先对变量的计算过程应用提炼函数(106)手法。
与复杂代码打交道时,细小的步子是快速前进的关键。
现在代码结构已经好多了。顶层的statement函数现在只剩7行代码,而且它处理的都是与打印详单相关的逻辑。与计算相关的逻辑从主函数中被移走,改由一组函数来支持。每个单独的计算过程和详单的整体结构,都因此变得更易理解了。
createStatementData.js
代码行数由我开始重构时的44行增加到了70行(不算htmlStatement),这主要是将代码抽取到函数里带来的额外包装成本。虽然代码的行数增加了,但重构也带来了代码可读性的提高。额外的包装将混杂的逻辑分解成可辨别的部分,分离了详单的计算逻辑与样式。这种模块化使我更容易辨别代码的不同部分,了解它们的协作关系。虽说言以简为贵,但可演化的软件却以明确为贵。通过增强代码的模块化,我可以轻易地添加HTML版本的代码,而无须重复计算部分的逻辑。
H.按类型重组计算过程
支持更多类型的戏剧,以及支持它们各自的价格计算和观众量积分计算。
创建演出计算器:
enrichPerformance函数是关键所在,因为正是它用每场演出的数据来填充中转数据结构。目前它直接调用了计算价格和观众量积分的函数,我需要创建一个类,通过这个类来调用这些函数。由于这个类存放了与每场演出相关数据的计算函数,于是我把它称为演出计算器(performance calculator)。
使用以工厂函数取代构造函数:
createStatementData.js
代码量仍然有所增加,因为我再次整理了代码结构。新结构带来的好处是,不同戏剧种类的计算各自集中到了一处地方。如果大多数修改都涉及特定类型的计算,像这样按类型进行分离就很有意义。当添加新剧种时,只需要添加一个子类,并在创建函数中返回它。
三.第一章结语
本章的重构有3个较为重要的节点,分别是:
(1)将原函数分解成一组嵌套的函数
(2)应用拆分阶段(154)分离计算逻辑与输出格式化逻辑
(3)为计算器引入多态性来处理计算逻辑
每一步都给代码添加了更多的结构,以便我能更好地表达代码的意图。
好代码的检验标准就是人们是否能轻而易举地修改它。好代码应该直截了当:有人需要修改代码时,他们应能轻易找到修改点,应该能快速做出更改,而不易引入其他错误。
一个健康的代码库能够最大限度地提升我们的生产力,支持我们更快、更低成本地为用户添加新特性。为了保持代码库的健康,就需要时刻留意现状与理想之间的差距,然后通过重构不断接近这个理想。
a.好代码的检验标准就是人们是否能轻而易举地修改它。
b.开展高效有序的重构,关键的心得是:
小的步子可以更快前进,请保持代码永远处于可工作状态,小步修改累积起来也能大大改善系统的设计。