《重构》 从一个小程序入手,开始我们的第一次重构
"如果它还可以运行,就不要动它。 " 如果一段代码能正常工作,并且不会再被修改,或许我们应该遵循这句古老的工程谚语,毕竟机器并不在乎你的代码结构是否丑陋。但代码需要维护,需求总在变动。如果有人看你写的代码时觉得很费劲,特别是那个人是一个月后的你自己时,那么,是时候改进它了。
脑袋能力有限,代码需要不断改进;记忆并不可靠,知识也需要时常温习。
时时勤拂拭,勿使惹尘埃
一、准备工作
我也想立刻开始我的第一次重构,但打开书我就被拦住了。书中的示例代码是Javascript,而我没有接触过这门语言。为了后续学习的流畅,必须得稍微准备下。
1.JavaScript运行环境与debug
毕竟不是文章主题,在这里只贴上我遇到的问题和解决的链接:
- vscode运行调试javascript代码
- SyntaxError: cannot use import statement outside a module
解决办法:在package.json文件中设置"type": “module”
npm Docs Creating a package.json file
Solved: Uncaught SyntaxError: cannot use import statement outside a module
语言都是相通的,示例代码比较简单,大概也都能看懂,解决了运行环境和debug问题,那我们就开始重构之旅。
二、重构步骤
1.起步(先理解场景和代码功能,代码我做了必要的注释,方便理解)
场景:一个戏剧演出团,根据观众人数及剧目类型来向客户收费
示例代码功能:1.根据客户观看信息,计算应收取的费用。 2.计算获取的积分(看剧获得积分,某种促销活动。相应规则我注释在代码里)
直接上代码
//演出ID(索引),演出名、类型
const playsData = {
"hamlet": {
"name": "Hamlet", "type": "tragedy" },
"as-like": {
"name": "As You Like It", "type": "comedy" },
"othello": {
"name": "Othello", "type": "tragedy" }
}
//记录顾客观看的演出ID和对应的人数
const invoicesData = {
"customer": "BigCo",
"performances": [
{
"playID": "hamlet", //ID
"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) {
//通过演出ID拿到演出名和类型
const play = plays[perf.playID];
let thisAmount = 0;
switch (play.type) {
//悲剧收费规则
case "tragedy":
thisAmount = 40000;
if (perf.audience > 30) {
thisAmount += 1000 * (perf.audience - 30);//人数超过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}`);
}
// 积分计算规则
volumeCredits += Math.max(perf.audience - 30, 0);
// 看喜剧额外加积分
if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);
// 拼接最后要打出的字符串
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;
}
console.log(statement(invoicesData,playsData));//调用函数并输出
//输出
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
代码组织不甚清晰,但这还在可忍受的限度内。书的作者觉得复杂的示例会让读者不忍卒读,所以设计了这一小段代码。这样短小的程序或许确实不需要进行深入的设计,但请想象它处于一个更大规模的项目中。
现在需求变动来了:1.希望也支持以HTML格式输出详单 ; 2.演员尝试更多的戏剧类型,对于新增的戏剧类型有新的计分方式。
读者可以思考下自己会怎样完成新功能,对问题1你会复制一份代码并修改其中字符串拼接的部分,以保证两种输出格式都支持? 对问题2是否会要增加switch里的分支并写下新的计费逻辑?
我再强调一次,是需求的变化使重构变得必要。如果一段代码能正常工作,并且不会再被修改,那么完全可以不去重构它。能改进之当然很好,但若没人需要去理解它,它就不会真正妨碍什么。
2.代码块分解(分解statement函数)
2.1提取switch语句,提炼函数(106)
先将这块代码抽取成一个独立的函数,按它所干的事情给它命名。作者在重构时会遵顼一些标准流程,他把这一步称作“提炼函数(106)”
每当看到这样长长的函数,我便下意识地想从整个函数中分离出不同的关注点。
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 = amountFor(play, perf);//---------------------这里
volumeCredits += Math.max(perf.audience - 30, 0);
if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);
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;
//-------------------------------------------------------------这里
//javascript支持函数写在函数内部
function amountFor(play, aPerformance) {
let result = 0;
switch (play.type) {
case "tragedy":
result = 40000;
if (aPerformance.audience > 30) {
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy":
result = 30000;
if (aPerformance.audience > 20) {
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default:
throw new Error(`unknown type: ${
play.type}`);
}
return result;
}
}
console.log(statement(invoicesData,playsData));
做完这个改动后,我会马上编译并执行一遍测试,看看有无破坏了其他东西。无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。犯错误是很容易的——至少我知道我是很容易犯错的。
!!!重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
这里有一些小tips,ide一般都对重构有支持,可以选择对应代码块后点击灯泡,按提示操作;提炼出来的函数,对于其用到的不会被改变的变量可以当作参数传入,会变动的变量(thisAmount)可以作为返回值; 对被提炼出的函数的函数名做合适的修改,让其能表达它的功能,对其中变量名修改,使其更简洁明确,比如返回值改为result。
立刻编译运行输出,检查功能是否被破坏。
2.2 对amountFor进行一些改进
paly可以由aPerformance计算得到,可以尝试删除它。
我喜欢将play这样的变量移除掉,因为它们创建了很多具有局部作用域的临时变量,这会使提炼函数更加复杂。这里我要使用的重构手法是以查询取代临时变量(178)
- 首先play赋值的表达式可以提取为
function playFor(perf) {
return plays[perf.playID];
}
- 使用内联变量(123)手法内联play变量和amount变量(就是把play和amount替换为对应函数)
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) {
volumeCredits += Math.max(perf.audience - 30, 0);
if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5);
result += ` ${
playFor(perf).name}: ${
format(amountFor(playFor(perf), perf) / 100)} (${
perf.audience} seats)\n`;
totalAmount += amountFor(playFor(perf), perf);
}
result += `Amount owed is ${
format(totalAmount / 100)}\n`;
result += `You earned ${
volumeCredits} credits\n`;
return result;
- 对amountFor函数应用改变函数声明(124)。 去除play参数。
- 修改参数名,最终结构如下
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) {
volumeCredits += Math.max