共计 57340 个字符,预计需要花费 144 分钟才能阅读完成。
最近,小李感觉公司女生们看他的眼神不太对劲了,那种笑容如同是充斥慈爱的、姨母般的笑容。
作为一名诚实本分的程序员,小李不太习惯这种被人适度关注的感觉,他不晓得产生了什么。
······
小李和小王的关系仿佛过于密切,还常常挤在一个工位上办公,始终到中午。
这个谰言直到某天他们的聊天内容被某个经营小姐姐听到,他们之间的机密才被大家发现。
小李和小王的故事
“这串代码看起来不太好。”小李指着屏幕,眉头紧锁。
“咋了,这段代码是我写的,因为这里要实现客户的一个定制化需要,所以看起来有点简单。”小王凑了过去。
“我也说不出来哪里不对劲,我当初要增加一个新个性,不晓得从哪里下手。”小李挠了挠头。
小王把凳子搬了过去:“来来,我给你讲讲,这段代码,这段调用 …..”
·····
中午 12 点,办公室的空气中弥漫着饭菜的香气,还有孑然一身约饭的小伙伴,休闲区也成了干饭的主战场。
“噢?原来小李和小王这种叫做结对编程?”经营小姐姐眼光扫向还在座位上的小李小王。
“嗯 … 偶然的结对编程是失常的,然而长期的结对编程阐明呈现了一些坏滋味。”老工程师说完,端起饭盆,干完了最初一口饭。
“oh… 是那种 … 滋味吗?”经营小姐姐试探道。
在座一起吃饭的 HR、UI 小姐姐们被这句话又点燃了八卦之魂,收回了一阵欢快的笑声。
“NoNoNo… 老耿的意思,代码里呈现了坏滋味。”高级工程师大宝扶了扶眼镜。
“什么叫代码里呈现了坏滋味呀?”
“就是软件架构要开始土崩瓦解的先兆,代码外面充斥着坏滋味,这些滋味都散发着恶心、腐烂的信号,提醒你该重构了。”
小姐姐们眉头一皱,不太称心大宝在吃饭的时候讲了个这么倒胃口的例子。
老耿喝了口水,放下水杯:“阿宝说的没错。很显然,小李和小王在写程序的时候,并没有发现代码中存在的坏滋味,导致他们当初欠下了一大批的技术债权,正在分期还债。”
“看来是时候给他们做个培训了,教他们如何辨认代码中的坏滋味。”
代码中的坏滋味
小李和小王顶着两个大黑眼圈来到会议室,老耿早曾经在会议室等着了。
小李看了一眼投影仪的内容“24 种常见的坏滋味及重构手法 —— 辞别加班,可继续倒退
”。
“呐,给你们俩筹备的咖啡,打起精神来哈。”大宝用身子推门进来,手上端着两杯热腾腾的咖啡,这是他刚从休息区亲手煮的陈腐咖啡。
“谢谢宝哥”小李和小王齐声道。
“据说你们俩最近老是一起加班到中午。”老耿把手从键盘上拿开,把眼帘从电脑屏幕上移到小李小王的身上。
“嘿嘿 …”小李不好意思的笑了笑“最近需要比拟难,加一些新性能比拟花工夫。”
“嗯,看来你们也发现了一些问题,为什么当初加新性能花的工夫会比以前多得多呢?”老耿把凳子往后挪了挪,看着两人。
“因为产品越来越简单了,加新性能也越来越花工夫了。”小王接话道。
“对,这算是一个起因。还有吗?”老耿接着问。
“因 … 因为咱们之前写的代码有点乱,可能互相理解起来比拟花工夫 …”小李挠了挠头。
“但其实我都写了正文的,我感觉还是因为产品太简单了。”小王对小李这个说法有点不认可。
“好,其实小李说到了一个关键问题。”老耿感觉是时候了,接着说道:“Martin Fowler 已经说过,当代码库看起来就像补丁摞补丁,须要粗疏的考古工作能力弄明确整个零碎是如何工作的。那这份累赘会一直拖慢新增性能的速度,到最初程序员巴不得从头开始重写整个零碎。”
“我想,你们应该有很屡次想重写零碎的激动吧?”老耿笑了笑,持续说道:“而外部品质良好的软件能够让我在增加新性能时,很容易找到在哪里批改、如何批改。”
老耿顿了顿,“所以,小王说的也对。产品简单带来了软件的简单,而简单的软件一不小心就容易变成了补丁摞补丁。软件代码中充斥着“坏滋味”,这些“坏滋味”最终会让软件腐烂,而后失去对它的掌控。”
“对了,小王方才提到的正文,也是一种坏滋味。”老耿笑了笑,看着小王,“所以,坏滋味无处不在,有的坏滋味比拟好觉察,有的坏滋味须要通过非凡训练,能力辨认。”
老耿又指了指投影仪大屏幕:“所以,咱们明天是一期非凡训练课,教会你们辨认 24 种常见的坏滋味及重构手法,这门课至多能够让你们成为一个有着一些特地好的习惯的还不错的程序员。”
“咳咳,你们俩赚大了”大宝补充道:“老耿这级别的大牛,在外边上课可是要免费的哟~”
“等等,我去拿笔记本。”小李举手示意,而后打开门走进来,小王见状也跟着小李进来拿笔记本了。
24 种常见的坏滋味及重构手法
老耿见大家曾经都做好了学习的筹备,站起身走到屏幕旁边,对着大家说道:“那咱们开始吧!”
”坏滋味咱们方才曾经说过了,我再讲个小故事吧。我这么多年以来,看过很多很多代码,它们所属的我的项目有大获胜利的,也有气息奄奄的。察看这些代码时,我和我的老搭档学会了从中找出某些特定构造,这些构造指出了 重构
的可能性,这些构造也就是我方才提到的 坏滋味
。”
“噢对,我方才还提到了一个词 —— 重构
。这听着像是个很可怕的词,然而我能够和你们说,我口中的 重构
并不是你们想的那种颠覆重来,有本书给了这个词一个全新的定义 —— 对软件内部结构的一种调整,目标是在不扭转软件可察看行为的前提下,进步其可了解性,升高其批改老本。
,你们也能够了解为是在 应用一系列重构手法,在不扭转软件可察看行为的前提下,调整其构造。
”
“如果有人说他们的代码在重构过程中有一两天工夫不可用,基本上能够确定,他们在做的事不是重构。”老耿开了个玩笑:“他们可能在对代码施展某种医治魔法,这种魔法带来的副作用就是会让软件短暂性休克。”
“在这里,还有一点值得一提,那就是如何做到 不扭转软件可察看行为
,这须要一些外力的帮助。这里我不倡议你们再把加班的人群延长到测试人员,我给出的计划是筹备一套齐备的、运行速度很快的测试套件。在绝大多数状况下,如果想要重构,就得先有一套能够自测试的代码。”
“接下来,我会先审查你们商城零碎的代码,发现代码中存在的坏滋味,而后增加单元测试,进行重构后,最初通过测试实现重构。”
“我会在这个过程中,给你们演示并解说 24 种常见的坏滋味及重构手法。”
“咱们开始吧!”老耿从新回到座位。
神秘命名(Mysterious Name)
function countOrder(order) {
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
const orderPrice = countOrder(order);
“下面的这段 countOrder
函数是做什么的?看到函数名的第一感觉不太清晰,统计订单?订单商品数量吗?还是统计什么?然而,看到函数外部实现后,我明确了这是个统计订单总价格的函数。这就是其中一个坏滋味 —— 神秘命名
,当代码中这样的坏滋味多了之后,花在猜谜上的工夫就会越来越多了。”
“咱们当初来对这段代码进行重构,咱们须要先增加单元测试代码。这里我只做演示,写两个测试用例。”
老耿很快针对这段代码,应用驰名的 jest
框架写出了上面两个测试用例。
describe('test price', () => {test('countOrder should return normal price when input correct order quantity < 500', () => {
const input = {
quantity: 20,
itemPrice: 10
};
const result = countOrder(input);
expect(result).toBe(220);
});
test('countOrder should return discount price when input correct order quantity > 500', () => {
const input = {
quantity: 1000,
itemPrice: 10
};
const result = countOrder(input);
expect(result).toBe(9850);
});
});
老耿 运行了一下测试用例
,显示测试通过后,说:“咱们有了单元测试后,就能够开始筹备重构工作了。”
“咱们先把 countOrder
外部的实现提炼成新函数,命名为 getPrice
,这个名字不肯定是最合适的名字,然而会比之前的要好。”老耿应用 Ide
很容易就把这一步实现了。
function getPrice(order) {
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
function countOrder(order) {return getPrice(order);
}
const orderPrice = countOrder(order);
“这一步看起来没什么问题,然而咱们还是先 运行一下测试用例
。”老耿按下了执行用例快捷键,用例跑了起来,并且很快就通过了测试。
“这一步阐明咱们的批改没有问题,下一步咱们批改测试用例,将测试用例调用的 countOrder
办法都批改为调用 getPrice
办法,再次 运行批改后的测试用例
。”
老耿指着批改后的测试用例:“再次运行后,getPrice
也通过了测试,那接下来,咱们就能够把调用 countOrder
办法的中央,都批改为调用 getPrice
办法,就像这样。”
function getPrice(order) {
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
function countOrder(order) {return getPrice(order);
}
const orderPrice = getPrice(order);
“这时候咱们能够看到,编辑器曾经提醒咱们,原来的 countOrder
办法没有被应用到,咱们能够借助 Ide 间接把这个函数删除掉。”
“删除后,咱们的重构就实现了,新的代码看起来像这样。”
function getPrice(order) {
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
const orderPrice = getPrice(order);
“嗯,这个命名看起来更适合了,一眼看过来就能够晓得这是个获取订单价格的函数。如果哪天又想到了更好的名字,用同样的步骤把它替换掉就好了,有单元测试就能够保障你在操作的过程中并不会出错,引发其余的问题。”
“对了,还有最重要的一步。”老耿减轻了语气:“记得提交代码!”
老耿用快捷键提交了一个 Commit
记录,持续说道:“每一次重构实现都应该提交代码,这样就能够在下一次重构呈现问题的时候,迅速回退到上一次失常工作时的状态,这一点很有用!“
大宝补充到:“这样的重构还有个益处,那就是能够保障代码随时都是可公布的状态,因为并没有影响到整体性能的运行。”
“不过想一个适合的好名字的确不容易,耿哥的意思是让咱们要继续的小步的进行重构吧。”小王摸着下巴思考说道。
“阿宝和小王说的都对,在不扭转软件可察看行为的前提下,继续小步的重构,保障软件随时都处于可公布的状态。这意味着咱们随时都能够进行重构,最简略的重构,比方我方才演示的那种用不了几分钟,而最长的重构也不该超过几小时。”
“我再补充一点。”大宝说道:“咱们相对不能漠视自动化测试,只有自动化测试能力保障在重构的过程中不扭转软件可察看行为,这一点看似不起眼,却是最最重要的要害之处。”
“阿宝说的没错,咱们至多要保障咱们重构的中央有单元测试,且能通过单元测试,能力算作是重构实现。”
老耿稍作进展后,期待大家了解本人方才的那段话后,接着说:“看来大家都开始感触到了重构的魅力,咱们最初看看这段代码重构前后的比照。”
“ok,那咱们接着说剩下的坏滋味吧。”
反复代码(Repeat Code)
function renderPerson(person) {const result = [];
result.push(`<p>${person.name}</p>`);
result.push(`<p>title: ${person.photo.title}</p>`);
result.push(emitPhotoData(person.photo));
return result.join('\n');
}
function photoDiv(photo) {return ['<div>', `<p>title: ${photo.title}</p>`, emitPhotoData(photo), '</div>'].join('\n');
}
function emitPhotoData(aPhoto) {const result = [];
result.push(`<p>location: ${aPhoto.location}</p>`);
result.push(`<p>date: ${aPhoto.date}</p>`);
return result.join('\n');
}
“嗯,这段代乍一看是没有什么问题的,应该是用来渲染个人资料界面的。然而咱们认真看的话,会发现 renderPerson
办法和 photoDiv
中有一个同样的实现,那就是渲染 photo.title
的局部。这一部分的逻辑总是在执行 emitPhotoData
函数的后面,这是一段反复代码。”
“尽管这是一段看似无伤大雅的反复代码,然而要记住,一旦有反复代码存在,浏览这些反复的代码时你就必须加倍认真,注意其间轻微的差别。如果要批改反复代码,你必须找出所有的副原本批改,这一点让人在浏览和批改代码时都很容易呈现纰漏。”
“所以,咱们就挑这一段代码来进行重构。依照常规,我先写两个单元测试用例。”老耿开始写用例。
describe('test render', () => {test('renderPerson should return correct struct when input correct struct', () => {
const input = {
name: 'jack',
photo: {
title: 'travel',
location: 'tokyo',
date: '2021-06-08'
}
};
const result = renderPerson(input);
expect(result).toBe(`<p>jack</p>\n<p>title: travel</p>\n<p>location: tokyo</p>\n<p>date: 2021-06-08</p>`);
});
test('photoDiv should return correct struct when input correct struct', () => {
const input = {
title: 'adventure',
location: 'india',
date: '2021-01-08'
};
const result = photoDiv(input);
expect(result).toBe(`<div>\n<p>title: adventure</p>\n<p>location: india</p>\n<p>date: 2021-01-08</p>\n</div>`);
});
});
“咱们先运行测试一下咱们的测试用例是否能通过吧。“老耿按下了执行快捷键。
“ok,测试通过,记得提交一个 Commit
,保留咱们的测试代码。接下来,咱们筹备开始重构,这个函数比较简单,咱们能够间接把那一行反复的代码移动到 emitPhotoData
函数中。然而这次咱们还是要演示一下危险较低的一种重构手法,避免出错。“老耿说完,把 emitPhotoDataNew
ctrl c + ctrl v,在复制的函数体内稍作批改,实现了组装。
function emitPhotoDataNew(aPhoto) {const result = [];
result.push(`<p>title: ${aPhoto.title}</p>`);
result.push(`<p>location: ${aPhoto.location}</p>`);
result.push(`<p>date: ${aPhoto.date}</p>`);
return result.join('\n');
}
“而后,咱们把 renderPerson
和 photoDiv
外部调用的办法,都换成 emitPhotoDataNew
新办法,如果再稳当一点的话,最好是换一个函数执行一次测试用例。”
function renderPerson(person) {const result = [];
result.push(`<p>${person.name}</p>`);
result.push(emitPhotoDataNew(person.photo));
return result.join('\n');
}
function photoDiv(photo) {return ['<div>', emitPhotoDataNew(photo), '</div>'].join('\n');
}
function emitPhotoData(aPhoto) {const result = [];
result.push(`<p>location: ${aPhoto.location}</p>`);
result.push(`<p>date: ${aPhoto.date}</p>`);
return result.join('\n');
}
function emitPhotoDataNew(aPhoto) {const result = [];
result.push(`<p>title: ${aPhoto.title}</p>`);
result.push(`<p>location: ${aPhoto.location}</p>`);
result.push(`<p>date: ${aPhoto.date}</p>`);
return result.join('\n');
}
“替换实现后,执行测试用例,看看成果。”
“ok,测试通过,阐明重构并没有产生什么问题,接下来把原来的 emitPhotoData
平安删除,而后把 emitPhotoDataNew
重命名为 emitPhotoData
,重构就实现了!”
function renderPerson(person) {const result = [];
result.push(`<p>${person.name}</p>`);
result.push(emitPhotoData(person.photo));
return result.join('\n');
}
function photoDiv(photo) {return ['<div>', emitPhotoData(photo), '</div>'].join('\n');
}
function emitPhotoData(aPhoto) {const result = [];
result.push(`<p>title: ${aPhoto.title}</p>`);
result.push(`<p>location: ${aPhoto.location}</p>`);
result.push(`<p>date: ${aPhoto.date}</p>`);
return result.join('\n');
}
“批改完后,别忘了运行测试用例。”老耿每次批改实现后运行测试用例的动作,仿佛曾经造成了肌肉记忆。
“ok,测试通过。这次重构实现了,提交一个 Commit
,再看一下批改前后的比照。”
“咱们持续看下一个坏滋味。”
过长函数(Long Function)
function printOwing(invoice) {
let outstanding = 0;
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
// calculate outstanding
for (const o of invoice.orders) {outstanding += o.amount;}
// record due date
const today = new Date(Date.now());
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
//print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
“嗯,这个函数看起来是用来打印用户白条信息的。这个函数实现细节和命名方面倒是没有太多问题,然而这里有一个坏滋味,那就是 —— 过长的函数。”
“函数越长,就越难了解。而更好的阐释力、更易于分享、更多的抉择——都是由小函数来反对的。”
“对于辨认这种坏滋味,有一个技巧。那就是,如果你须要花工夫浏览一段代码能力弄清它到底在干什么,那么就应该将其提炼到一个函数中,并依据它所做的事为其命名。”
“像这个函数就是那种须要花肯定工夫浏览能力弄清楚在做什么的,不过好在这个函数还有些正文。”
“还是老办法,咱们先写两个测试用例。”老耿开始敲代码。
describe('test printOwing', () => {let collections = [];
console.log = message => {collections.push(message);
};
afterEach(() => {collections = [];
});
test('printOwing should return correct struct when input correct struct', () => {
const input = {
customer: 'jack',
orders: [{amount: 102}, {amount: 82}, {amount: 87}, {amount: 128}]
};
printOwing(input);
expect(collections).toStrictEqual([
'***********************',
'**** Customer Owes ****',
'***********************',
'name: jack',
'amount: 399',
'due: 7/8/2021'
]);
});
test('printOwing should return correct struct when input correct struct 2', () => {
const input = {
customer: 'dove',
orders: [{amount: 63}, {amount: 234}, {amount: 12}, {amount: 1351}]
};
printOwing(input);
expect(collections).toStrictEqual([
'***********************',
'**** Customer Owes ****',
'***********************',
'name: dove',
'amount: 1660',
'due: 7/8/2021'
]);
});
});
“测试用例写完当前运行一下。“
“接下来的提取步骤就很简略了,因为代码自身是有正文的,咱们只须要参考正文的节奏来进行提取就好了,仍旧是小步慢跑,首先调整函数执行的程序,将函数分层。”
function printOwing(invoice) {
// print banner
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
// calculate outstanding
let outstanding = 0;
for (const o of invoice.orders) {outstanding += o.amount;}
// record due date
const today = new Date(Date.now());
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
// print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
“进行函数分层后,先运行一遍测试用例,避免调整程序的过程中,影响了函数性能 … ok,测试通过了。”
“被调整程序后的函数就变得非常简单了,接下来咱们分四步提取就能够了”
“第一步,提炼 printBanner
函数,而后运行测试用例。”
function printBanner() {console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function printOwing(invoice) {printBanner();
// calculate outstanding
let outstanding = 0;
for (const o of invoice.orders) {outstanding += o.amount;}
// record due date
const today = new Date(Date.now());
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
// print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
“咱们在提取的过程中,把正文也去掉了,因为的确不须要了,函数名和正文的内容一样。”
“第二步,提炼 calOutstanding
函数,而后运行测试用例。”
function printBanner() {console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function calOutstanding(invoice) {
let outstanding = 0;
for (const o of invoice.orders) {outstanding += o.amount;}
return outstanding;
}
function printOwing(invoice) {printBanner();
let outstanding = calOutstanding(invoice);
// record due date
const today = new Date(Date.now());
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
// print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
“第三步,提炼 recordDueDate
函数,而后运行测试用例。”
function printBanner() {console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function calOutstanding(invoice) {
let outstanding = 0;
for (const o of invoice.orders) {outstanding += o.amount;}
return outstanding;
}
function recordDueDate(invoice) {const today = new Date(Date.now());
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
function printOwing(invoice) {printBanner();
let outstanding = calOutstanding(invoice);
recordDueDate(invoice);
// print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
“第四步,提炼 printDetails
函数,而后运行测试用例。”
function printBanner() {console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function calOutstanding(invoice) {
let outstanding = 0;
for (const o of invoice.orders) {outstanding += o.amount;}
return outstanding;
}
function recordDueDate(invoice) {const today = new Date(Date.now());
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
function printDetails(invoice, outstanding) {console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
function printOwing(invoice) {printBanner();
let outstanding = calOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
“测试用例通过后,别忘了提交代码。”
“而后咱们再来扫视一下这个重构后的 printOwing
函数,简略的四行代码,清晰的形容了函数所做的事件,这就是小函数的魅力!”
“说到底,让小函数易于了解的要害还是在于良好的命名。如果你能给函数起个好名字,浏览代码的人就能够通过名字理解函数的作用,基本不用去看其中写了些什么。这能够节约大量的工夫,也能缩小你们结对编程的工夫。”老耿面带微笑看着小李和小王。
小李小王相视一笑,感觉有点不好意思。
“咱们来看看重构前后的比照。”
“咱们持续。”老耿快马加鞭。
过长参数列表(Long Parameter List)
// range.js
function priceRange(products, min, max, isOutSide) {if (isOutSide) {
return products
.filter(r => r.price < min || r.price > max);
} else {
return products
.filter(r => r.price > min && r.price < max);
}
}
// a.js
const range = {min: 1, max: 10}
const outSidePriceProducts = priceRange([ /* ... */],
range.min,
range.max,
true
)
// b.js
const range = {min: 5, max: 8}
const insidePriceProducts = priceRange([ /* ... */],
range.min,
range.max,
false
)
“第一眼看过来,priceRange
是过滤商品的函数,认真看的话会发现,次要比对的是 product.price
字段和传入的参数 min
与 max
之间的大小比照关系。如果 isOutSide
为 true
的话,则过滤出价格区间之外的商品,否则过滤出价格区间之内的商品。”
“第一眼看过来,这个函数的参数切实是太多了,这会让客户端调用方感到很纳闷。还好参数 isOutSide
的命名还算不错,不然这个函数会看起来更深奥。”
小李忍不住插了句:“我每次调用 priceRange
函数的时候都要去看一眼这个函数的实现,我老是遗记最初一个参数的规定。”
“我和小李的认识是一样的。”老耿点了拍板:“我也不喜爱标记参数,因为它们让人难以了解到底有哪些函数能够调用、应该怎么调用。应用这样的函数,我还得弄清标记参数有哪些可用的值。”
“既然小李也曾经发现这个问题了,那咱们就从这个 isOutSide
参数下手,进行优化。老规矩,咱们先针对现有的代码写两个测试用例。”老耿开始写代码。
describe('test priceRange', () => {test('priceRange should return correct result when input correct outside conditional', () => {
const products = [{ name: 'apple', price: 6},
{name: 'banana', price: 7},
{name: 'orange', price: 15},
{name: 'cookie', price: 0.5}
];
const range = {min: 1, max: 10};
const isOutSide = true;
const result = priceRange(products, range.min, range.max, isOutSide);
expect(result).toStrictEqual([{ name: 'orange', price: 15},
{name: 'cookie', price: 0.5}
]);
});
test('priceRange should return correct result when input correct inside conditional', () => {
const products = [{ name: 'apple', price: 6},
{name: 'banana', price: 7},
{name: 'orange', price: 15},
{name: 'cookie', price: 0.5}
];
const range = {min: 5, max: 8};
const isOutSide = false;
const result = priceRange(products, range.min, range.max, isOutSide);
expect(result).toStrictEqual([{ name: 'apple', price: 6},
{name: 'banana', price: 7}
]);
});
});
“运行一下单元测试 … 嗯,是能够通过的。那接下来就能够进行参数精简了,咱们先把方才小李提的那个问题解决,就是标记参数,咱们针对 priceRange
再提炼两个函数。”
“咱们先批改咱们的单元测试代码,按咱们冀望调用的形式批改。”
const priceRange = require('./long_parameter_list');
describe('test priceRange', () => {test('priceOutSideRange should return correct result when input correct outside conditional', () => {
const products = [{ name: 'apple', price: 6},
{name: 'banana', price: 7},
{name: 'orange', price: 15},
{name: 'cookie', price: 0.5}
];
const range = {min: 1, max: 10};
const result = priceOutSideRange(products, range.min, range.max);
expect(result).toStrictEqual([{ name: 'orange', price: 15},
{name: 'cookie', price: 0.5}
]);
});
test('priceInsideRange should return correct result when input correct inside conditional', () => {
const products = [{ name: 'apple', price: 6},
{name: 'banana', price: 7},
{name: 'orange', price: 15},
{name: 'cookie', price: 0.5}
];
const range = {min: 5, max: 8};
const result = priceInsideRange(products, range.min, range.max);
expect(result).toStrictEqual([{ name: 'apple', price: 6},
{name: 'banana', price: 7}
]);
});
});
“我把 priceRange
的 isOutSide
标记参数移除了,并且应用 priceOutsideRange
和 priceInsideRange
两个办法来实现原有的性能。这时候还不能运行测试用例,因为咱们的代码还没改呢。同样的,把代码调整成合乎用例调用的形式。”
function priceRange(products, min, max, isOutSide) {if (isOutSide) {return products.filter(r => r.price < min || r.price > max);
} else {return products.filter(r => r.price > min && r.price < max);
}
}
function priceOutSideRange(products, min, max) {return priceRange(products, min, max, true);
}
function priceInsideRange(products, min, max) {return priceRange(products, min, max, false);
}
“代码调整实现后,咱们来运行一下测试用例。好的,通过了!”
“嗯,我想到这里当前,能够更进一步,把 priceRange
的函数进一步抽离,就像这样。”
function priceRange(products, min, max, isOutSide) {if (isOutSide) {return products.filter(r => r.price < min || r.price > max);
} else {return products.filter(r => r.price > min && r.price < max);
}
}
function priceOutSideRange(products, min, max) {return products.filter(r => r.price < min || r.price > max);
}
function priceInsideRange(products, min, max) {return products.filter(r => r.price > min && r.price < max);
}
“拆解实现后,记得运行一下测试用例 … ok,通过了”
“在测试用例通过后,就能够开始筹备迁徙工作了。把原来调用 priceRange
的中央替换成新的调用,而后再把 priceRange
函数平安删除,就像这样。”
// range.js
function priceOutSideRange(products, min, max) {return products.filter(r => r.price < min || r.price > max);
}
function priceInsideRange(products, min, max) {return products.filter(r => r.price > min && r.price < max);
}
// a.js
const range = {min: 1, max: 10}
const outSidePriceProducts = priceOutSideRange([ /* ... */],
range.min,
range.max
)
// b.js
const range = {min: 5, max: 8}
const insidePriceProducts = priceInsideRange([ /* ... */],
range.min,
range.max
)
“这么做当前,原来让人纳闷的标记参数就被移除了,取而代之的是两个语义更加清晰的函数。”
“接下来,咱们要持续做一件有价值的重构,那就是将数据组织成构造,因为这样让数据项之间的关系变得清晰。比方 range
的 min
和 max
总是在调用中被一起应用,那这两个参数就能够组织成构造。我先批改我的测试用例以适应最新的改变,就像这样。”
//...
const range = {min: 1, max: 10};
const result = priceOutSideRange(products, range);
expect(result).toStrictEqual([{ name: 'orange', price: 15},
{name: 'cookie', price: 0.5}
]);
//...
const range = {min: 5, max: 8};
const result = priceInsideRange(products, range);
expect(result).toStrictEqual([{ name: 'apple', price: 6},
{name: 'banana', price: 7}
]);
“测试用例批改实现后,来批改一下咱们的函数。”
// range.js
function priceOutSideRange(products, range) {return products.filter(r => r.price < range.min || r.price > range.max);
}
function priceInsideRange(products, range) {return products.filter(r => r.price > range.min && r.price < range.max);
}
// a.js
const range = {min: 1, max: 10}
const outSidePriceProducts = priceOutSideRange([ /* ... */],
range
)
// b.js
const range = {min: 5, max: 8}
const insidePriceProducts = priceInsideRange([ /* ... */],
range
)
“批改实现后,运行咱们的测试用例,顺利通过,别忘了提交代码。”说完,老耿打了个 Commit
。
“这一步重构又精简了一个参数,这是这项重构最间接的价值。而这项重构真正的意义在于,它会催生代码中更深层次的扭转。一旦辨认出新的数据结构,我就能够重组程序的行为来应用这些构造。这句话理论利用起来是什么意思呢?我还是拿这个案例来举例。”
“咱们会发现 priceOutSideRange
和 priceInsideRange
的函数命名曾经足够清晰,然而外部对 range
范畴的断定还是须要破费肯定工夫了解,而 range
作为咱们刚辨认进去的一种构造,能够持续进行重构,就像这样。”
// range.js
class Range {constructor(min, max) {
this._min = min;
this._max = max;
}
outside(num) {return num < this._min || num > this._max;}
inside(num) {return num > this._min && num < this._max;}
}
function priceOutSideRange(products, range) {return products.filter(r => range.outside(r.price));
}
function priceInsideRange(products, range) {return products.filter(r => range.inside(r.price));
}
// a.js
const outSidePriceProducts = priceOutSideRange([ /* ... */],
new Range(1, 10)
)
// b.js
const insidePriceProducts = priceInsideRange([ /* ... */],
new Range(5, 8)
)
“批改测试用例也传入 Range
对象,而后运行测试用例 …ok,通过了。测试通过后再提交代码。”
“这样一来,让 priceOutSideRange
和 priceInsideRange
函数外部也更加清晰了。同时,range
被组织成了一种新的数据结构,这种构造能够在任何计算区间的中央应用。”
“咱们来看看重构前后的比照。”
“咱们持续。”
全局数据(Global Data)
// global.js
// ...
let userAuthInfo = {
platform: 'pc',
token: ''
}
export {userAuthInfo};
// main.js
userAuthInfo.token = localStorage.token;
// request.js
const reply = await login();
userAuthInfo.token = reply.data.token;
// business.js
await request({authInfo: userAuthInfo});
“这个 global.js
仿佛是用来提供全局数据的,这是最刺鼻的坏滋味之一了。”
“这个 platform
被全局都应用到了,我能够把它批改为别的值吗?会引发什么问题吗?”老耿问道
小李连忙说:“这个 platform
不能改,后端要靠这个字段来抉择辨认 token
的形式,改了就会出问题。”
“然而我当初能够在代码库的任何一个角落都能够批改 platform
和 token
,而且没有任何机制能够探测出到底哪段代码做出了批改,这就是全局数据的问题。”
“每当咱们看到可能被各处的代码净化的数据,咱们还是须要全局数据用一个函数包装起来,至多你就能看见批改它的中央,并开始管制对它的拜访,这里我做个简略的封装,而后再写两个测试用例。”
let userAuthInfo = {
platform: 'pc',
token: ''
};
function getUserAuthInfo() {return { ...userAuthInfo};
}
function setToken(token) {userAuthInfo.token = token;}
export {
getUserAuthInfo,
setToken
}
// main.js
setToken(localStorage.token);
// request.js
const reply = await login();
setToken(reply.data.token);
// business.js
await request({authInfo: getUserAuthInfo() });
“接下来运行一下测试用例。”
describe("test global data", () => {test('getUserAuthInfo.platform should return pc when modify reference', () => {const userAuthInfo = getUserAuthInfo();
userAuthInfo.platform = 'app';
const result = getUserAuthInfo().platform;
expect(result).toBe('pc');
});
test('getUserInfo.token should return test-token when setToken test-token', () => {setToken('test-token');
const result = getUserAuthInfo().token;
expect(result).toBe('test-token');
});
});
“这样一来,通过对象援用就无奈批改源对象了,并且我这里管制了对 platform
属性的批改,只凋谢对 token
批改的接口。即便如此,咱们还是要尽可能的防止全局数据,因为全局数据是最刺鼻的坏滋味之一!”老耿语气减轻。
小李小王疯狂拍板。
“咱们来看一下重构前后的比照。”
“那咱们持续。”
可变数据(Mutable Data)
function merge(target, source) {for (const key in source) {target[key] = source[key];
}
return target;
}
“这个函数如同有点古老。”老耿有些纳闷。
“这个是我从之前的仓库 copy 过去的一个工具函数,用来合成对象的,始终没改过。”小王补充道。
“嗯,这个函数的问题是对 merge
对象的源对象 target
进行了批改,对数据的批改常常导致出其不意的后果和难以发现的 bug。当初来看程序并没有因为这个函数呈现问题,但如果故障只在很常见的状况下产生,要找出故障起因就会更加艰难。”
“先写两个测试用例来进行验证吧。”老耿开始写代码。
describe('test merge', () => {test('test merge should return correct struct when merge', () => {
const baseConfig = {
url: 'https://api.com',
code: 'mall'
};
const testSpecialConfig = {
url: 'https://test-api.com',
code: 'test-mall'
};
const result = merge(baseConfig, testSpecialConfig);
expect(result).toStrictEqual({
url: 'https://test-api.com',
code: 'test-mall'
});
});
test('test merge should return original struct when merge', () => {
const baseConfig = {
url: 'https://api.com',
code: 'mall'
};
const testSpecialConfig = {
url: 'https://test-api.com',
code: 'test-mall'
};
merge(baseConfig, testSpecialConfig);
expect(baseConfig).toStrictEqual({
url: 'https://api.com',
code: 'mall'
});
});
});
“运行一下 … 第二个用例报错了。”
“报错的起因就是因为对源对象进行了批改调整,从而影响了 baseConfig
的值。接下来咱们调整一下 merge
函数就行了,当初 javascript
有很简略的办法能够批改这个函数。”
function merge(target, source) {
return {
...target,
...source
}
}
“批改实现后,再次运行用例,就能够看到用例运行通过了。”
“我方才的重构手法其实有一整个软件开发流派 —— 函数式编程,就是齐全建设在“数据永不扭转”的概念根底上:如果要更新一个数据结构,就返回一份新的数据正本,旧的数据仍放弃不变,这样能够防止很多因数据变动而引发的问题。”
“在方才介绍全局数据时用到的封装变量的办法,也是对可变数据这种坏滋味常见的一种解决方案。还有,如果可变数据的值能在其余中央计算出来,这就是一个特地刺鼻的坏滋味。它不仅会造成困扰、bug 和加班,而且毫无必要。”
“这里我就不做开展了,如果你们俩感兴趣的话,能够去看看《重构:改善既有代码的设计(第 2 版)》这本书,我方才提到的坏滋味,书外面都有。”
小李小王奋笔疾书,把书名记了下来。
“咱们来看一下重构前后的比照。”
“那咱们持续。”
发散式变动(Divergent Change)
function getPrice(order) {
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
const orderPrice = getPrice(order);
“这个函数是咱们最早重构的函数,它的职责就是计算根底价格 – 数量折扣 + 运费。咱们再来看看这个函数,如果根底价格计算规定扭转,须要批改这个函数,如果折扣规定产生扭转也须要批改这个函数,同理,运费计算规定也会引发它的扭转。”
“如果某个模块常常因为不同的起因在不同的方向上发生变化,发散式变动就呈现了。”
“测试用例曾经有了,所以咱们能够间接对函数进行重构。”老耿开始写代码。
function calBasePrice(order) {return order.quantity * order.itemPrice;}
function calDiscount(order) {return Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
}
function calShipping(basePrice) {return Math.min(basePrice * 0.1, 100);
}
function getPrice(order) {return calBasePrice(order) - calDiscount(order) + calShipping(calBasePrice(order));
}
const orderPrice = getPrice(order);
“批改实现后,咱们运行之前写的测试用例 … 测试通过了。”
“批改之后的三个函数只须要关怀本人职责方向的变动就能够了,而不是一个函数关注多个方向的变动。并且,单元测试粒度还能够写的更细一点,这样对排查问题的效率也有很大的晋升。”
大宝适时补充了一句:“其实这就是面向对象设计准则中的 繁多职责准则
。”
“阿宝说的没错,keep simple,每次只关怀一个上下文
这一点始终很重要。”
“咱们来看一下重构前后的比照。”
“咱们持续。”
霰弹式批改(Shotgun Surgery)
// File Reading.js
const reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};
function acquireReading() { return reading};
function baseRate(month, year) {/* */}
// File 1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
// File 2
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
function taxThreshold(year) {/* */}
// File 3
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);
function calculateBaseCharge(aReading) {return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
“接下来要说的就是 发散式变动
的反例 —— 霰弹式批改
。这个要找一个软件里的案例须要花点工夫,这里我间接拿《重构》原书的一个例子来作解说。”
“其实这个问题和反复代码有点像,反复代码常常会引起霰弹式批改的问题。”
“像下面的演示代码,如果 reading
的局部逻辑产生了扭转,对这部分逻辑的批改须要逾越好几个文件调整。”
“如果每遇到某种变动,你都必须在许多不同的类或文件内做出许多小批改,你所面临的坏滋味就是霰弹式批改。如果须要批改的代码分布到处,你岂但很难找到它们,也很容易错过某个重要的批改。”
“这里我来对这段代码进行重构,因为源代码也不是很残缺,这里我只把批改的思路提一下,就不写测试代码了。”老耿开始写代码。
// File Reading.js
class Reading {constructor(data) {
this._customer = data.customer;
this._quantity = data.quantity;
this._month = data.month;
this._year = data.year;
}
get customer() {return this._customer;}
get quantity() {return this._quantity;}
get month() {return this._month;}
get year() {return this._year;}
get baseRate() {/* ... */}
get baseCharge() {return baseRate(this.month, this.year) * this.quantity;
}
get taxableCharge() {return Math.max(0, base - taxThreshold());
}
get taxThreshold() {/* ... */}
}
const reading = new Reading({customer: 'ivan', quantity: 10, month: 5, year: 2017});
“在批改实现后,所有和 reading
相干的逻辑都放在一起治理了,并且我把它组合成一个类当前还有一个益处。那就是类能明确地给这些函数提供一个共用的环境,在对象外部调用这些函数能够少传许多参数,从而简化函数调用,并且这样一个对象也能够更不便地传递给零碎的其余局部。”
“如果你们在编码过程中,有遇到我方才提到的那些问题,那就是一种坏滋味。下次就能够用相似的重构手法进行重构了,当然,别忘了写测试用例。”老耿对着小李二人说道。
小李小王疯狂拍板。
“咱们来看一下重构前后的比照。”
“那咱们持续。”
依恋情节(Feature Envy)
class Account {constructor(data) {
this._name = data.name;
this._type = data.type;
}
get loanAmount() {if (this._type.type === 'vip') {return 20000;} else {return 10000;}
}
}
class AccountType {constructor(type) {this._type = type;}
get type() {return this._type;}
}
“这段代码是账户 Account
和账户类型 AccountType
,如果账户的类型是 vip
,贷款额度 loanAmount
就有 20000,否则就只有 10000。”
“在获取贷款额度时,Account
外部的 loanAmount
办法和另一个类 AccountType
的外部数据交换分外频繁,远胜于在本人所处模块外部的交换,这就是依恋情结的典型状况。”
“咱们先写两个测试用例吧。”老耿开始写代码。
describe('test Account', () => {test('Account should return 20000 when input vip type', () => {
const input = {
name: 'jack',
type: new AccountType('vip')
};
const result = new Account(input).loanAmount;
expect(result).toBe(20000);
});
test('Account should return 20000 when input normal type', () => {
const input = {
name: 'dove',
type: new AccountType('normal')
};
const result = new Account(input).loanAmount;
expect(result).toBe(10000);
});
});
“测试用例能够间接运行 … ok,通过了。”
“接下来,咱们把 loanAmount
搬移到真正属于它的中央。”
class Account {constructor(data) {
this._name = data.name;
this._type = data.type;
}
get loanAmount() {return this._type.loanAmount;}
}
class AccountType {constructor(type) {this._type = type;}
get type() {return this._type;}
get loanAmount() {if (this.type === 'vip') {return 20000;} else {return 10000;}
}
}
“在搬移实现后,loanAmount
拜访的都是本身模块的数据,不再依恋其余模块。咱们运行一下测试用例。”
“ok,测试通过了,别忘了提交代码。”老耿提交了一个 commit。
“咱们来看一下重构前后的比照。”
“咱们持续下一个。”
数据泥团(Data Clumps)
class Person {constructor(name) {this._name = name;}
get name() {return this._name;}
set name(arg) {this._name = arg;}
get telephoneNumber() {return `(${this.officeAreaCode}) ${this.officeNumber}`;
}
get officeAreaCode() {return this._officeAreaCode;}
set officeAreaCode(arg) {this._officeAreaCode = arg;}
get officeNumber() {return this._officeNumber;}
set officeNumber(arg) {this._officeNumber = arg;}
}
const person = new Person('jack');
person.officeAreaCode = '+86';
person.officeNumber = 18726182811;
console.log(`person's name is ${person.name}, telephoneNumber is ${person.telephoneNumber}`);
// person's name is jack, telephoneNumber is (+86) 18726182811
“这个 Person 类记录了用户的名字(name),电话区号(officeAreaCode)和电话号码(officeNumber),这里有一个不是很刺鼻的坏滋味。”
“如果我把 officeNumber
字段删除,那 officeAreaCode
就失去了意义。这阐明这两个字段总是一起呈现的,除了 Person
类,其余用到电话号码的中央也是会呈现这两个字段的组合。”
“这个坏滋味叫做数据泥团,次要体现在数据项喜爱孑然一身地待在一块儿。你经常能够在很多中央看到雷同的三四项数据:两个类中雷同的字段、许多函数签名中雷同的参数,这些总是绑在一起呈现的数据真应该领有属于它们本人的对象。”
“老规矩,咱们先写两个测试用例。”老耿开始写代码
describe('test Person', () => {test('person.telephoneNumber should return (+86) 18726182811 when input correct struct', () => {const person = new Person('jack');
person.officeAreaCode = '+86';
person.officeNumber = 18726182811;
const result = person.telephoneNumber;
expect(person.officeAreaCode).toBe('+86');
expect(person.officeNumber).toBe(18726182811);
expect(result).toBe('(+86) 18726182811');
});
test('person.telephoneNumber should return (+51) 15471727172 when input correct struct', () => {const person = new Person('jack');
person.officeAreaCode = '+51';
person.officeNumber = 15471727172;
const result = person.telephoneNumber;
expect(person.officeAreaCode).toBe('+51');
expect(person.officeNumber).toBe(15471727172);
expect(result).toBe('(+51) 15471727172');
});
});
“运行一下测试用例 … ok,测试通过了,筹备开始重构了。”
“咱们先新建一个 TelephoneNumber
类,用于合成 Person
类所承当的责任。”
class TelephoneNumber {constructor(areaCode, number) {
this._areaCode = areaCode;
this._number = number;
}
get areaCode() {return this._areaCode;}
get number() {return this._number;}
toString() {return `(${this._areaCode}) ${this._number}`;
}
}
“这时候,咱们再调整一下咱们的 Person
类,应用新的数据结构。”
class Person {constructor(name) {
this._name = name;
this._telephoneNumber = new TelephoneNumber();}
get name() {return this._name;}
set name(arg) {this._name = arg;}
get telephoneNumber() {return this._telephoneNumber.toString();
}
get officeAreaCode() {return this._telephoneNumber.areaCode;}
set officeAreaCode(arg) {this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber);
}
get officeNumber() {return this._telephoneNumber.number;}
set officeNumber(arg) {this._telephoneNumber = new TelephoneNumber(this.officeAreaCode, arg);
}
}
“重构实现,咱们运行测试代码。”
“测试用例运行通过了,别忘了提交代码。”
“在这里我抉择新建一个类,而不是简略的记录构造,是因为一旦领有新的类,你就有机会让程序散发出一种芬芳。失去新的类当前,你就能够着手寻找其余坏滋味,例如“依恋情节”,这能够帮你指出可能移至新类中的种种行为。这是一种弱小的能源:有用的类被创立进去,大量的反复被打消,后续开发得以减速,原来的数据泥团终于在它们的小社会中充分发挥价值。”
“比方这里,TelephoneNumber
类被提炼进去后,就能够去毁灭那些应用到 telephoneNumber
的反复代码,并且依据应用状况进一步优化,我就不做开展了。”
“咱们来看一下重构前后的比照。”
“咱们持续讲下一个坏滋味。”
根本类型偏执(Primitive Obsession)
class Product {constructor(data) {
this._name = data.name;
this._price = data.price;
/* ... */
}
get name() {return this.name;}
/* ... */
get price() {return `${this.priceCount} ${this.priceSuffix}`;
}
get priceCount() {return parseFloat(this._price.slice(1));
}
get priceUnit() {switch (this._price.slice(0, 1)) {
case '¥':
return 'cny';
case '$':
return 'usd';
case 'k':
return 'hkd';
default:
throw new Error('un support unit');
}
}
get priceCnyCount() {switch (this.priceUnit) {
case 'cny':
return this.priceCount;
case 'usd':
return this.priceCount * 7;
case 'hkd':
return this.priceCount * 0.8;
default:
throw new Error('un support unit');
}
}
get priceSuffix() {switch (this.priceUnit) {
case 'cny':
return '元';
case 'usd':
return '美元';
case 'hkd':
return '港币';
default:
throw new Error('un support unit');
}
}
}
“咱们来看看这个 Product
(产品)类,大家应该也看进去了这个类的一些坏滋味,price
字段作为一个根本类型,在 Product
类中被各种转换计算,而后输入不同的格局,Product
类须要关怀 price
的每一个细节。”
“在这里,price
十分值得咱们为它创立一个属于它本人的根本类型 – Price
。”
“在重构之前,先把测试用例笼罩残缺。”老耿开始写代码。
describe('test Product price', () => {
const products = [{ name: 'apple', price: '$6'},
{name: 'banana', price: '¥7'},
{name: 'orange', price: 'k15'},
{name: 'cookie', price: '$0.5'}
];
test('Product.price should return correct price when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).price);
expect(result).toStrictEqual(['6 美元', '7 元', '15 港币', '0.5 美元']);
});
test('Product.price should return correct priceCount when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).priceCount);
expect(result).toStrictEqual([6, 7, 15, 0.5]);
});
test('Product.price should return correct priceUnit when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).priceUnit);
expect(result).toStrictEqual(['usd', 'cny', 'hkd', 'usd']);
});
test('Product.price should return correct priceCnyCount when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).priceCnyCount);
expect(result).toStrictEqual([42, 7, 12, 3.5]);
});
test('Product.price should return correct priceSuffix when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).priceSuffix);
expect(result).toStrictEqual(['美元', '元', '港币', '美元']);
});
});
“测试用例写完当前运行一下,看看成果。”
“这个重构手法也比较简单,先新建一个 Price
类,先把 price
和相干的行为搬移到 Price
类中,而后再委托给 Product
类即可。咱们先来实现 Price
类。”
class Price {constructor(value) {this._value = value;}
toString() {return `${this.count} ${this.suffix}`;
}
get count() {return parseFloat(this._value.slice(1));
}
get unit() {switch (this._value.slice(0, 1)) {
case '¥':
return 'cny';
case '$':
return 'usd';
case 'k':
return 'hkd';
default:
throw new Error('un support unit');
}
}
get cnyCount() {switch (this.unit) {
case 'cny':
return this.count;
case 'usd':
return this.count * 7;
case 'hkd':
return this.count * 0.8;
default:
throw new Error('un support unit');
}
}
get suffix() {switch (this.unit) {
case 'cny':
return '元';
case 'usd':
return '美元';
case 'hkd':
return '港币';
default:
throw new Error('un support unit');
}
}
}
“此时,Product
类我还没有批改,然而如果你感觉你搬移函数的过程中容易手抖不释怀的话,能够运行一下测试用例。”
“接下来是重构 Product
类,将原有跟 price
相干的逻辑,应用中间人委托来调用。”
class Product {constructor(data) {
this._name = data.name;
this._price = new Price(data.price);
/* ... */
}
get name() {return this.name;}
/* ... */
get price() {return this._price.toString();
}
get priceCount() {return this._price.count;}
get priceUnit() {return this._price.unit;}
get priceCnyCount() {return this._price.cnyCount;}
get priceSuffix() {return this._price.suffix;}
}
“重构实现后,运行测试用例。”老耿按下运行键。
“测试用例运行通过了,别忘了提交代码。”
“很多人对根本类型都有一种偏爱,他们广泛感觉根本类型要比类简洁,然而,别让这种偏爱演变成了 偏执
。有些时候,咱们须要走出传统的洞窟,进入煊赫一时的对象世界。”
“这个案例演示了一种很常见的场景,置信你们当前也能够辨认根本类型偏执这种坏滋味了。”
小李小王疯狂拍板。
“咱们来看一下重构前后的比照。”
“那咱们持续吧。”
反复的 switch(Repeated switch)
class Price {constructor(value) {this._value = value;}
toString() {return `${this.count} ${this.suffix}`;
}
get count() {return parseFloat(this._value.slice(1));
}
get unit() {switch (this._value.slice(0, 1)) {
case '¥':
return 'cny';
case '$':
return 'usd';
case 'k':
return 'hkd';
default:
throw new Error('un support unit');
}
}
get cnyCount() {switch (this.unit) {
case 'cny':
return this.count;
case 'usd':
return this.count * 7;
case 'hkd':
return this.count * 0.8;
default:
throw new Error('un support unit');
}
}
get suffix() {switch (this.unit) {
case 'cny':
return '元';
case 'usd':
return '美元';
case 'hkd':
return '港币';
default:
throw new Error('un support unit');
}
}
}
“方才咱们提炼了 Price
类后,当初发现 Price
类有个问题,你们看进去了吗?”老耿看着小李小王。
小李摇了点头,小王也没谈话。
“反复的 switch
语句,每当看到代码里有 switch
语句时,就要提高警惕了。当看到反复的 switch
语句时,这种坏滋味就冒出来了。”老耿接着说道。
“反复的 switch 的问题在于:每当你想减少一个抉择分支时,必须找到所有的 switch,并逐个更新。”
“并且这种 switch
构造是十分软弱的,频繁的批改 switch
语句可能还可能会引发别的问题,置信你们也遇到过这种状况。”
小李此时仿佛想起了什么,补充道:“这里的 switch
语句还好,有些中央的 switch
语句写的太长了,每次了解起来也很艰难,所以容易改出问题。”
“小李说的不错,那咱们当初来重构这个 Price
。这里我偷个懒,测试用例接着用之前 Product
的测试用例,你们能够在理论我的项目中针对 Price
写用例,测试用例的粒度越小,越容易定位问题。”
“咱们先创立一个工厂函数,同时将 Product
类的实例办法也应用工厂函数创立。”老耿开始写代码。
class Product {constructor(data) {
this._name = data.name;
this._price = createPrice(data.price);
/* ... */
}
/* ... */
}
function createPrice(value) {return new Price(value);
}
“运行一下测试用例 … ok,通过了。那咱们下一步,把 Price
作为超类,创立一个子类 CnyPrice
,继承于 Price
,同时批改工厂函数,在货币类型为 ¥
时,创立并返回 CnyPrice
类。”
class CnyPrice extends Price {constructor(props) {super(props);
}
}
function createPrice(value) {switch (value.slice(0, 1)) {
case '¥':
return new CnyPrice(value);
default:
return new Price(value);
}
}
“运行一下测试用例 … ok,通过了。那咱们下一步,把 Price
超类中,所有对于 cny
的条件逻辑的函数,在 CnyPrice
中进行重写。”
class CnyPrice extends Price {constructor(props) {super(props);
}
get unit() {return 'cny';}
get cnyCount() {return this.count;}
get suffix() {return '元';}
}
“重写实现后,运行一下测试用例 … ok,通过了,下一步再把 Price
类中,所有对于 cny
的条件分支都移除。”
class Price {constructor(value) {this._value = value;}
toString() {return `${this.count} ${this.suffix}`;
}
get count() {return parseFloat(this._value.slice(1));
}
get unit() {switch (this._value.slice(0, 1)) {
case '$':
return 'usd';
case 'k':
return 'hkd';
default:
throw new Error('un support unit');
}
}
get cnyCount() {switch (this.unit) {
case 'usd':
return this.count * 7;
case 'hkd':
return this.count * 0.8;
default:
throw new Error('un support unit');
}
}
get suffix() {switch (this.unit) {
case 'usd':
return '美元';
case 'hkd':
return '港币';
default:
throw new Error('un support unit');
}
}
}
“移除实现后,运行一下测试用例。”
“运行通过,接下来咱们如法炮制,把 UsdPrice
和 HkdPrice
也创立好,最初再将超类中的条件分支逻辑相干代码都移除。”老耿持续写代码。
class Price {constructor(value) {this._value = value;}
toString() {return `${this.count} ${this.suffix}`;
}
get count() {return parseFloat(this._value.slice(1));
}
get suffix() {throw new Error('un support unit');
}
}
class CnyPrice extends Price {constructor(props) {super(props);
}
get unit() {return 'cny';}
get cnyCount() {return this.count;}
get suffix() {return '元';}
}
class UsdPrice extends Price {constructor(props) {super(props);
}
get unit() {return 'usd';}
get cnyCount() {return this.count * 7;}
get suffix() {return '美元';}
}
class HkdPrice extends Price {constructor(props) {super(props);
}
get unit() {return 'hkd';}
get cnyCount() {return this.count * 0.8;}
get suffix() {return '港币';}
}
function createPrice(value) {switch (value.slice(0, 1)) {
case '¥':
return new CnyPrice(value);
case '$':
return new UsdPrice(value);
case 'k':
return new HkdPrice(value);
default:
throw new Error('un support unit');
}
}
“重构实现后,运行测试用例。”
“ok,运行通过,别忘了提交代码。”
“这样一来,批改对应的货币逻辑并不影响其余的货币逻辑,并且增加一种新的货币规定也不会影响到其余货币逻辑,批改和增加个性都变得简略了。”
“简单的条件逻辑是编程中最难了解的货色之一,最好能够将条件逻辑拆分到不同的场景,从而拆解简单的条件逻辑。这种拆分有时用条件逻辑自身的构造就足以表白,但应用类和多态能把逻辑的拆分表述得更清晰。”
“就像我方才演示的那样。”
“咱们来看一下重构前后的比照。“
“那咱们持续吧。”
循环语句(Loop)
function acquireCityAreaCodeData(input, country) {const lines = input.split('\n');
let firstLine = true;
const result = [];
for (const line of lines) {if (firstLine) {
firstLine = false;
continue;
}
if (line.trim() === '') continue;
const record = line.split(',');
if (record[1].trim() === country) {result.push({ city: record[0].trim(), phone: record[2].trim()});
}
}
return result;
}
“嗯,让我看看这个函数,看名字仿佛是获取城市区号信息,我想理解一下这个函数的外部实现。嗯,它的实现,先是疏忽了第一行,而后疏忽了为空的字符串,而后将字符串以逗号切割,而后 …”
“尽管有点绕,但花些工夫还是能看进去实现逻辑的。”
“从最早的编程语言开始,循环就始终是程序设计的外围因素。但我感觉现在循环曾经有点儿过期。”
“随着时代在倒退,现在越来越多的编程语言都提供了更好的语言构造来解决迭代过程,例如 Javascript
的数组就有很多管道办法。”
“是啊,ES
都曾经出到 ES12
了。”小王感叹,有点学不动了。
“哈哈,有些新个性还是给咱们的重构工作提供了很多帮忙的,我来演示一下这个案例。演示之前,还是先补充两个测试用例。”老耿开始写代码。
describe('test acquireCityData', () => {test('acquireCityData should return India city when input India', () => {
const input =
',,+00\nMumbai,India,+91 22\n , , \nTianjing,China,+022\n , , \nKolkata,India,+91 33\nBeijing,China,+010\nHyderabad,India,+91 40';
const result = acquireCityData(input, 'India');
expect(result).toStrictEqual([
{
city: 'Mumbai',
phone: '+91 22'
},
{
city: 'Kolkata',
phone: '+91 33'
},
{
city: 'Hyderabad',
phone: '+91 40'
}
]);
});
test('acquireCityData should return China city when input China', () => {
const input =
',,+00\nMumbai,India,+91 22\n , , \nTianjing,China,+022\n , , \nKolkata,India,+91 33\nBeijing,China,+010\nHyderabad,India,+91 40';
const result = acquireCityData(input, 'China');
expect(result).toStrictEqual([
{
city: 'Tianjing',
phone: '+022'
},
{
city: 'Beijing',
phone: '+010'
}
]);
});
});
“写完测试用例后,运行一下 … ok,通过了。”接下来筹备重构工作。
“像这样比较复杂的函数,咱们抉择一步一步拆解。首先,把疏忽第一行,间接用 slice
代替。”
function acquireCityData(input, country) {let lines = input.split('\n');
const result = [];
lines = lines.slice(1);
for (const line of lines) {if (line.trim() === '') continue;
const record = line.split(',');
if (record[1].trim() === country) {result.push({ city: record[0].trim(), phone: record[2].trim()});
}
}
return result;
}
“批改实现后,运行测试用例 … ok,下一步过滤为空的 line
,这里能够用到 filter
。”
function acquireCityData(input, country) {let lines = input.split('\n');
const result = [];
lines = lines.slice(1).filter(line => line.trim() !== '');
for (const line of lines) {const record = line.split(',');
if (record[1].trim() === country) {result.push({ city: record[0].trim(), phone: record[2].trim()});
}
}
return result;
}
“批改实现后,运行测试用例 … ok,下一步是将 line
用 split
切割,能够应用 map
。”
function acquireCityData(input, country) {let lines = input.split('\n');
const result = [];
lines = lines
.slice(1)
.filter(line => line.trim() !== '')
.map(line => line.split(','));
for (const line of lines) {if (line[1].trim() === country) {result.push({ city: line[0].trim(), phone: line[2].trim()});
}
}
return result;
}
“批改实现后,运行测试用例 … ok,下一步是判断国家,能够用 filter
。”
function acquireCityData(input, country) {let lines = input.split('\n');
const result = [];
lines = lines
.slice(1)
.filter(line => line.trim() !== '')
.map(line => line.split(','))
.filter(record => record[1].trim() === country);
for (const line of lines) {result.push({ city: line[0].trim(), phone: line[2].trim()});
}
return result;
}
“批改实现后,运行测试用例 … ok,最初一步是数据组装,能够应用 map
。”
function acquireCityData(input, country) {let lines = input.split('\n');
return lines
.slice(1)
.filter(line => line.trim() !== '')
.map(line => line.split(','))
.filter(record => record[1].trim() === country)
.map(record => ({ city: record[0].trim(), phone: record[2].trim()}));
}
“重构实现,运行测试用例。”
“测试通过,重构实现了,别忘了提交代码。”
“重构实现后,再看这个函数,咱们就能够发现,管道操作能够帮忙咱们更快地看清楚被解决的元素以及解决它们的动作。”
“可是。”小王举手:“在性能上,循环要比管道的性能要好吧?”
“这是个好问题,但这个问题要从三个方面来解释。”
“首先,这一部分工夫会被用在两个中央,一是用来做性能优化让程序运行的更快,二是因为不足对程序的分明意识而破费工夫。”
“那我先说一下性能优化,如果你对大多数程序进行剖析,就会发现它把大半工夫都消耗在一小半代码身上。如果你厚此薄彼地优化所有代码,90 %的优化工作都是白吃力的,因为被你优化的代码大多很少被执行。”
“第二个方面来说,尽管重构可能使软件运行更慢,但它也使软件的性能优化更容易,因为重构后的代码让人对程序能有更分明的意识。”
“第三个方面来说,随着古代电脑硬件倒退和浏览器技术倒退,很多以前会影响性能的重构手法,例如小函数,当初都不会造成性能的影响。以前所认知的性能影响观点也须要与时俱进。”
“这里须要引入一个更高的概念,那就是应用适合的性能度量工具,真正对系统进行性能剖析。哪怕你齐全理解零碎,也请理论度量它的性能,不要臆测。臆测会让你学到一些货色,但十有八九你是错的。”
“所以,我给出的倡议是:除了对性能有严格要求的实时零碎,其余任何状况下“编写疾速软件”的机密就是:先写出可调优的软件,而后调优它以求取得足够的速度。短期看来,重构确实可能使软件变慢,但它使优化阶段的软件性能调优更容易,最终还是会失去好的成果。”
“咱们来看一下重构前后的比照。”
“那咱们持续下一个。”
冗赘的元素(Lazy Element)
function reportLines(aCustomer) {const lines = [];
gatherCustomerData(lines, aCustomer);
return lines;
}
function gatherCustomerData(out, aCustomer) {out.push(["name", aCustomer.name]);
out.push(["location", aCustomer.location]);
}
“有些函数不能明确的说存在什么问题,然而能够优化。比方这个函数,能给代码减少构造,设计之初可能是为了反对变动、促成复用或者哪怕只是提供更好的名字,但在这里看来真的不须要这层额定的构造。因为,它的名字就跟实现代码看起来截然不同。”
“有些时候也并不齐全是因为适度设计,也可能是因为随着重构的进行越变越小,最初只剩了一个函数。”
“这里我间接用内联函数把它优化掉。先写两个测试用例。”老耿开始写代码。
describe('test reportLines', () => {test('reportLines should return correct array struct when input aCustomer', () => {
const input = {
name: 'jack',
location: 'tokyo'
};
const result = reportLines(input);
expect(result).toStrictEqual([['name', 'jack'],
['location', 'tokyo']
]);
});
test('reportLines should return correct array struct when input aCustomer', () => {
const input = {
name: 'jackli',
location: 'us'
};
const result = reportLines(input);
expect(result).toStrictEqual([['name', 'jackli'],
['location', 'us']
]);
});
});
“运行一下测试用例 … ok,没有问题,那咱们开始重构吧。”老耿开始写代码。
function reportLines(aCustomer) {const lines = [];
lines.push(["name", aCustomer.name]);
lines.push(["location", aCustomer.location]);
return lines;
}
“ok,很简略,重构实现了,咱们运行测试用例。”
“用例测试通过了。如果你想再精简一点,能够再批改一下。”
function reportLines(aCustomer) {
return [['name', aCustomer.name],
['location', aCustomer.location]
];
}
“运行测试用例 … 通过了,提交代码。”
“在重构的过程中会发现越来越多能够重构的新构造,就像我方才演示的那样。”
“像这类的冗赘的元素存在并没有太多的帮忙,所以,让它们慷慨赴义去吧。”
“咱们来看看重构前后的比照。”
“咱们持续。”
沉默寡言通用性(Speculative Generality)
class TrackingInformation {get shippingCompany() {return this._shippingCompany;}
set shippingCompany(arg) {this._shippingCompany = arg;}
get trackingNumber() {return this._trackingNumber;}
set trackingNumber(arg) {this._trackingNumber = arg;}
get display() {return `${this.shippingCompany}: ${this.trackingNumber}`;
}
}
class Shipment {get trackingInfo() {return this._trackingInformation.display;}
get trackingInformation() { return this._trackingInformation;}
set trackingInformation(aTrackingInformation) {this._trackingInformation = aTrackingInformation;}
}
“嗯 … 来看看这个对于这两个物流的类,而 TrackingInformation
记录物流公司和物流单号,而 Shipment
只是应用 TrackingInformation
治理物流信息,并没有其余任何额定的工作。为什么用一个额定的 TrackingInformation
来治理物流信息,而不是间接用 Shipment
来治理呢?”
“因为 Shipment
可能还会有其余的职责。”小王示意这是本人写的代码。“所以,我应用了一个额定的类来追踪物流信息。”
“很好,繁多职责准则。”
“那这个 Shipment
存在多久了,我看看代码提交记录 …”老耿看着 git 信息说道:“嗯,曾经存在两年了,目前看来它还没有呈现其余的职责,我要再等它几年吗?”
“这个坏滋味是非常敏感的。”老耿顿了顿,接着说道:“零碎里存在一些 沉默寡言通用性
的设计,常见语句就是 咱们总有一天会用上的
,并因而希图以各式各样的钩子和非凡状况来解决一些非必要的事件,这么做的后果往往造成零碎更难了解和保护。“
“在重构之前,咱们先写两个测试用例吧。”老耿开始写代码。
describe('test Shipment', () => {test('Shipment should return correct trackingInfo when input trackingInfo', () => {
const input = {
shippingCompany: '顺丰',
trackingNumber: '87349189841231'
};
const result = new Shipment(input.shippingCompany, input.trackingNumber).trackingInfo;
expect(result).toBe('顺丰: 87349189841231');
});
test('Shipment should return correct trackingInfo when input trackingInfo', () => {
const input = {
shippingCompany: '中通',
trackingNumber: '1281987291873'
};
const result = new Shipment(input.shippingCompany, input.trackingNumber).trackingInfo;
expect(result).toBe('中通: 1281987291873');
});
});
“当初还不能运行测试用例,为什么呀?”老耿自问自答:“因为这个用例运行是必定会报错的,Shipment
目前的构造基本不反对这么调用的,所以必定会出错。”
“这里我要引入一个新的概念,那就是 TDD – 测试驱动开发。”
“测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现性能的帽子,在测试的辅助下,疾速实现其性能;再戴上重构的帽子,在测试的爱护下,通过去除冗余的代码,进步代码品质。测试驱动着整个开发过程:首先,驱动代码的设计和性能的实现;其后,驱动代码的再设计和重构。”
“这里,咱们就是先写出咱们心愿程序运行的形式,再通过测试用例去反推程序设计,在通过测试用例后,性能也算是开发实现了。”
“上面咱们进行代码重构。”老耿开始写代码。
class Shipment {constructor(shippingCompany, trackingNumber) {
this._shippingCompany = shippingCompany;
this._trackingNumber = trackingNumber;
}
get shippingCompany() {return this._shippingCompany;}
set shippingCompany(arg) {this._shippingCompany = arg;}
get trackingNumber() {return this._trackingNumber;}
set trackingNumber(arg) {this._trackingNumber = arg;}
get trackingInfo() {return `${this.shippingCompany}: ${this.trackingNumber}`;
}
}
“我把 TrackingInformation
类齐全移除了,应用 Shipment
间接对物流信息进行治理。在重构实现后,运行测试用例。”
“用例运行通过了,这时候再把之前利用到 Shipment
的中央进行调整。当然,更稳当的方法是先应用 ShipmentNew
类进行替换后,再删除原来的类。这里我还是回退一下代码,你们俩去评估一下影响点,再本人来重构吧。”老耿回退了代码。
小李小王疯狂拍板。
“对于代码通用性设计,如果所有安装都会被用到,就值得那么做;如果用不到,就不值得。用不上的安装只会挡你的路,所以,把它搬开吧。”
“咱们来看看重构前后的比照。”
“咱们持续吧。”
长期字段(Temporary Field)
class Site {constructor(customer) {this._customer = customer;}
get customer() {return this._customer;}
}
class Customer {constructor(data) {
this._name = data.name;
this._billingPlan = data.billingPlan;
this._paymentHistory = data.paymentHistory;
}
get name() {return this._name;}
get billingPlan() {return this._billingPlan;}
set billingPlan(arg) {this._billingPlan = arg;}
get paymentHistory() {return this._paymentHistory;}
}
// Client 1
{
const aCustomer = site.customer;
// ... lots of intervening code ...
let customerName;
if (aCustomer === 'unknown') customerName = 'occupant';
else customerName = aCustomer.name;
}
// Client 2
{const plan = aCustomer === 'unknown' ? registry.billingPlans.basic : aCustomer.billingPlan;}
// Client 3
{if (aCustomer !== 'unknown') aCustomer.billingPlan = newPlan;
}
// Client 4
{const weeksDelinquent = aCustomer === 'unknown' ? 0 : aCustomer.paymentHistory.weeksDelinquentInLastYear;}
“这一段代码是,咱们的线下商城服务点,在老客户搬走新客户还没搬进来的时候,会呈现临时没有客户的状况。在每个查问客户信息的中央,都须要判断这个服务点有没有客户,而后再依据判断来获取无效信息。”
“aCustomer === 'unknown'
这是个特例状况,在这个特例状况下,就会应用到很多长期字段,或者说是非凡值字段。这种反复的判断不仅会来反复代码的问题,也会十分影响外围逻辑的代码可读性,造成了解的艰难。”
“这里,我要把所有的反复判断逻辑都移除掉,放弃外围逻辑代码的纯正性。而后,我要把这些长期字段收拢到一个中央,进行对立治理。咱们先写两个测试用例。”
describe('test Site', () => {test('Site should return correct data when input Customer', () => {
const input = {
name: 'jack',
billingPlan: {num: 100, offer: 50},
paymentHistory: {weeksDelinquentInLastYear: 28}
};
const result = new Site(new Customer(input)).customer;
expect({
name: result.name,
billingPlan: result.billingPlan,
paymentHistory: result.paymentHistory
}).toStrictEqual(input);
});
test('Site should return empty data when input NullCustomer', () => {
const input = {
name: 'jack',
billingPlan: {num: 100, offer: 50},
paymentHistory: {weeksDelinquentInLastYear: 28}
};
const result = new Site(new NullCustomer(input)).customer;
expect({
name: result.name,
billingPlan: result.billingPlan,
paymentHistory: result.paymentHistory
}).toStrictEqual({
name: 'occupant',
billingPlan: {num: 0, offer: 0},
paymentHistory: {weeksDelinquentInLastYear: 0}
});
});
});
“嗯,这次又是 TDD,第一个用例是能够运行的,运行是能够通过的。”
“接下来,我按这个思路去实现 NullCustomer
,这个实现起来其实很简略。”
class NullCustomer extends Customer {constructor(data) {super(data);
this._name = 'occupant';
this._billingPlan = {num: 0, offer: 0};
this._paymentHistory = {weeksDelinquentInLastYear: 0};
}
}
“实现实现后,运行一下测试用例。”
“我引入了这个特例对象后,我只须要在初始化 Site
的时候判断老客户搬出新客户还没有搬进来的状况,决定初始化哪一个 Customer
,而不必在每个调用的中央都判断一次,还引入那么多长期字段了。”
“如果写进去的话,就像是这样一段伪代码。”
// initial.js
const site = customer === 'unknown' ? new Site(new NullCustomer()) : new Site(new Customer(customer));
// Client 1
{
const aCustomer = site.customer;
// ... lots of intervening code ...
const customerName = aCustomer.name;
}
// Client 2
{const plan = aCustomer.billingPlan;}
// Client 3
{
}
// Client 4
{const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;}
“在这里我就不对你们的代码做理论批改了,你们上来当前本人调整一下吧。”
小李小王疯狂拍板。
“咱们来看一下重构前后的比照。”
“咱们持续下一个。”
过长的音讯链(Message Chains)
const result = a(b(c(1, d(f()))));
“这种坏滋味我手写代码演示一下,比方向一个对象申请另一个对象,而后再向后者申请另一个对象,而后再申请另一个对象……这就是音讯链。在理论代码中,看到的可能就是一长串取值函数或一长串长期变量。”
“这种一长串的取值函数,能够应用的重构手法就是 提炼函数
,就像这样。”
const result = goodNameFunc();
function goodNameFunc() {return a(b(c(1, d(f()))));
}
“再给提炼进去的函数,取一个好名字就行了。”
“还有一种状况,就是委托关系,须要暗藏委托关系。我就不做开展了,你们有趣味的话去看一看重构那本书吧。“
“咱们来看一下重构前后的比照。”
咱们持续下一个。”
中间人(Middle Man)
class Product {constructor(data) {
this._name = data.name;
this._price = createPrice(data.price);
/* ... */
}
get name() {return this.name;}
/* ... */
get price() {return this._price.toString();
}
get priceCount() {return this._price.count;}
get priceUnit() {return this._price.unit;}
get priceCnyCount() {return this._price.cnyCount;}
get priceSuffix() {return this._price.suffix;}
}
“嗯,这个 Product
+ Price
又被我翻出来了,因为通过了两次重构后,它还是存在一些坏滋味。”
“当初我要拜访 Product
价格相干的信息,都是间接通过 Product
拜访,而 Product
负责提供 price
的很多接口。随着 Price
类的新个性越来越多,更多的转发函数就会使人焦躁,而当初曾经有点让人焦躁了。”
“这个 Product
类曾经快齐全变成一个中间人了,那我当初心愿调用方应该间接应用 Price
类。咱们先来写两个测试用例。”老耿开始写代码。
describe('test Product price', () => {
const products = [{ name: 'apple', price: '$6'},
{name: 'banana', price: '¥7'},
{name: 'orange', price: 'k15'},
{name: 'cookie', price: '$0.5'}
];
test('Product.price should return correct price when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).price.toString());
expect(result).toStrictEqual(['6 美元', '7 元', '15 港币', '0.5 美元']);
});
test('Product.price should return correct priceCount when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).price.count);
expect(result).toStrictEqual([6, 7, 15, 0.5]);
});
test('Product.price should return correct priceUnit when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).price.unit);
expect(result).toStrictEqual(['usd', 'cny', 'hkd', 'usd']);
});
test('Product.price should return correct priceCnyCount when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).price.cnyCount);
expect(result).toStrictEqual([42, 7, 12, 3.5]);
});
test('Product.price should return correct priceSuffix when input products', () => {const input = [...products];
const result = input.map(item => new Product(item).price.suffix);
expect(result).toStrictEqual(['美元', '元', '港币', '美元']);
});
});
“写完的测试用例也是不能间接运行的,接下来咱们调整 Product
类,把中间人移除。”
class Product {constructor(data) {
this._name = data.name;
this._price = createPrice(data.price);
/* ... */
}
get name() {return this.name;}
/* ... */
get price() {return this._price;}
}
“调整实现后,间接运行测试用例。”
“测试用例通过了,别忘了把应用到 Product
的中央都查看一遍。”
“很难说什么水平的暗藏才是适合的。然而有暗藏委托关系和删除中间人,就能够在零碎运行过程中一直进行调整。随着代码的变动,“适合的暗藏水平”这个尺度也相应扭转。”
“咱们来看看重构前后的比照。”
“咱们持续下一个吧。”
底细交易(Insider Trading)
class Person {constructor(name) {this._name = name;}
get name() {return this._name;}
get department() {return this._department;}
set department(arg) {this._department = arg;}
}
class Department {get code() {return this._code;}
set code(arg) {this._code = arg;}
get manager() {return this._manager;}
set manager(arg) {this._manager = arg;}
}
“在这个案例里,如果要获取 Person
的部门代码 code
和部门领导 manager
都须要先获取 Person.department
。这样一来,调用者须要额定理解 Department
的接口细节,如果 Department
类批改了接口,变动会波及通过 Person
对象应用它的所有客户端。”
“咱们都喜爱在模块之间建起高墙,极其恶感在模块之间大量替换数据,因为这会减少模块间的耦合。在理论状况里,肯定的数据交换不可避免,但我必须尽量减少这种状况,并把这种替换都放到明面上来。”
“接下来,咱们依照咱们冀望程序运行的形式,来编写两个测试用例。”
describe('test Person', () => {test('Person should return 88 when input Department code 88', () => {
const inputName = 'jack'
const inputDepartment = new Department();
inputDepartment.code = 88;
inputDepartment.manager = 'Tom';
const result = new Person(inputName, inputDepartment).departmentCode;
expect(result).toBe(88);
});
test('Person should return Tom when input Department manager Tom', () => {
const inputName = 'jack'
const inputDepartment = new Department();
inputDepartment.code = 88;
inputDepartment.manager = 'Tom';
const result = new Person(inputName, inputDepartment).manager;
expect(result).toBe('Tom');
});
});
“在测试用例中,咱们能够间接通过 Person
失去这个人的部门代码 departmentCode
和部门领导 manager
了,那接下来,咱们把 Person
类进行重构。”
class Person {constructor(name, department) {
this._name = name;
this._department = department;
}
get name() {return this._name;}
get departmentCode() {return this._department.code;}
set departmentCode(arg) {this._department.code = arg;}
get manager() {return this._department._manager;}
set manager(arg) {this._department._manager = arg;}
}
“这里我间接将批改一步到位,然而你们练习的时候还是要一小步一小步进行重构,发现问题就能够间接回退代码。”老耿苦口婆心的说道。
小李小王疯狂拍板。
“咱们回来看代码,在代码里,我把委托关系进行了暗藏,从而客户端对 Department
类的依赖。这么一来,即便未来委托关系发生变化,变动也只会影响服务对象 – Person
类,而不会间接波及所有客户端。”
“咱们运行一下测试代码。”
“运行通过了,在所有代码替换实现前,能够先保留对 department
的拜访,在所有代码都批改实现后,再齐全移除,提交代码。”
“咱们来看看重构前后的比照。”
“咱们持续下一个。”
过大的类(Large Class)
“还有一种坏滋味叫做 过大的类
,这里我不必举新的例子了,最早的 Product
类其实就存在这样的问题。”
“如果想利用单个类做太多事件,其内往往就会呈现太多字段。一旦如此,反复代码也就接踵而至了。二来,过大的类也会造成了解的艰难。过大的类和过长的函数都有相似的问题。”
“咱们在 Product
类中就发现了三个坏滋味:根本类型偏执、反复的 switch、中间人。在解决这三个坏滋味的过程中,也把 过大的类
这个问题给解决了。”
“重构是继续的小步的,你们能够对 Product
类除了 price
以外的办法再进行屡次提炼,我这里就不再演示了。”
小李小王疯狂拍板。
“那咱们持续讲下一个。”
殊途同归的类(Alternative Classes with Different Interfaces)
class Employee {constructor(name, id, monthlyCost) {
this._id = id;
this._name = name;
this._monthlyCost = monthlyCost;
}
get monthlyCost() {return this._monthlyCost;}
get name() {return this._name;}
get id() {return this._id;}
get annualCost() {return this.monthlyCost * 12;}
}
class Department {constructor(name, staff) {
this._name = name;
this._staff = staff;
}
get staff() {return this._staff.slice();
}
get name() {return this._name;}
get totalMonthlyCost() {return this.staff.map(e => e.monthlyCost).reduce((sum, cost) => sum + cost);
}
get headCount() {return this.staff.length;}
get totalAnnualCost() {return this.totalMonthlyCost * 12;}
}
“这里有一个坏滋味,和反复代码有殊途同归之妙,叫做殊途同归的类。这里我以经典的 Employee
案例来解说一下。”
“在这个案例中,Employee
类和 Department
都有 name
字段,也都有月度老本 monthlyCost
和年度老本 annualCost
的概念,能够说这两个类其实在做相似的事件。”
“咱们能够用提炼超类来组织这种殊途同归的类,来打消反复行为。”
“在此之前,依据咱们最初想要实现的成果,咱们先编写两个测试用例。”
describe('test Employee and Department', () => {test('Employee annualCost should return 600 when input monthlyCost 50', () => {
const input = {
name: 'Jack',
id: 1,
monthlyCost: 50
};
const result = new Employee(input.name, input.id, input.monthlyCost).annualCost;
expect(result).toBe(600);
});
test('Department annualCost should return 888 when input different staff', () => {
const input = {
name: 'Dove',
staff: [{monthlyCost: 12}, {monthlyCost: 41}, {monthlyCost: 24}, {monthlyCost: 32}, {monthlyCost: 19}]
};
const result = new Department(input.name, input.staff).annualCost;
expect(result).toBe(1536);
});
});
“这个测试用例当初运行也是失败的,因为咱们还没有把 Department
革新实现。接下来,咱们先把 Employee
和 Department
雷同的字段和行为提炼进去,提炼成一个超类 Party
。”
class Party {constructor(name) {this._name = name;}
get name() {return this._name;}
get monthlyCost() {return 0;}
get annualCost() {return this.monthlyCost * 12;}
}
“这两个类雷同的字段有 name
,还有计算年度老本 annualCost
的形式,因为应用到了 monthlyCost
字段,所以我把这个字段也提炼进去,先返回个默认值 0。”
“接下来对 Employee
类进行精简,将提炼到超类的局部进行继承。”
class Employee extends Party {constructor(name, id, monthlyCost) {super(name);
this._id = id;
this._monthlyCost = monthlyCost;
}
get monthlyCost() {return this._monthlyCost;}
get id() {return this._id;}
}
“再接下来对 Department
类进行革新,继承 Party
类,而后进行精简。”
class Department extends Party {constructor(name, staff) {super(name);
this._staff = staff;
}
get staff() {return this._staff.slice();
}
get monthlyCost() {return this.staff.map(e => e.monthlyCost).reduce((sum, cost) => sum + cost);
}
get headCount() {return this.staff.length;}
}
“这样就实现了革新,运行一下测试用例。”
“测试通过了。记得把其余应用到这两个类的中央革新实现后再提交代码。”
“如果看见两个殊途同归的类在做类似的事,能够利用根本的继承机制把它们的相似之处提炼到超类。”
“有很多时候,正当的继承关系是在程序演变的过程中才浮现进去的:我发现了一些独特元素,心愿把它们抽取到一处,于是就有了继承关系。所以,先尝试用小而快的重构手法,重构后再发现新的可重构构造。”
“咱们来看一下重构前后的比照。”
“咱们持续下一个。”
纯数据类(Data Class)
class Category {constructor(data) {
this._name = data.name;
this._level = data.level;
}
get name() {return this._name;}
set name(arg) {this._name = arg;}
get level() {return this._level;}
set level(arg) {this._level = arg;}
}
class Product {constructor(data) {
this._name = data._name;
this._category = data.category;
}
get category() {return `${this._category.level}.${this._category.name}`;
}
}
“Category
是个纯数据类,像这样的纯数据类,间接应用字面量对象仿佛也没什么问题。”
“然而,纯数据类经常意味着行为被放在了谬误的中央。比方在 Product
有一个应该属于 Category
的行为,就是转化为字符串,如果把解决数据的行为从其余中央搬移到纯数据类里来,就能使这个纯数据类有存在的意义。”
“咱们先写两个简略的测试用例。”老耿开始写代码。
describe('test Category', () => {test('Product.category should return correct data when input category', () => {
const input = {
level: 1,
name: '水果'
};
const result = new Product({name: '苹果', category: new Category(input) }).category;
expect(result).toBe('1. 水果');
});
test('Product.category should return correct data when input category', () => {
const input = {
level: 2,
name: '热季水果'
};
const result = new Product({name: '苹果', category: new Category(input) }).category;
expect(result).toBe('2. 热季水果');
});
});
“测试用例写完当前,运行一下 … ok,通过了。接下来,咱们把本应该属于 Category
的行为,挪进来。”
class Category {constructor(data) {
this._name = data.name;
this._level = data.level;
}
get name() {return this._name;}
set name(arg) {this._name = arg;}
get level() {return this._level;}
set level(arg) {this._level = arg;}
toString() {return `${this._level}.${this._name}`;
}
}
class Product {constructor(data) {
this._name = data._name;
this._category = data.category;
}
get category() {return this._category.toString();
}
}
“而后咱们运行一下测试用例。”
“用例运行胜利了,别忘了提交代码。”老耿打了个 commit。
“咱们须要为纯数据赋予行为,或者应用纯数据类来控制数据的读写。否则的话,纯数据类并没有太大存在的意义,应该作为冗赘元素被移除。”
“咱们来看一下重构前后的比照。”
“那咱们持续下一个。”
被回绝的遗赠(Refuse Bequest)
class Party {constructor(name, staff) {
this._name = name;
this._staff = staff;
}
get staff() {return this._staff.slice();
}
get name() {return this._name;}
get monthlyCost() {return 0;}
get annualCost() {return this.monthlyCost * 12;}
}
class Employee extends Party {constructor(name, id, monthlyCost) {super(name);
this._id = id;
this._monthlyCost = monthlyCost;
}
get monthlyCost() {return this._monthlyCost;}
get id() {return this._id;}
}
class Department extends Party {constructor(name) {super(name);
}
get monthlyCost() {return this.staff.map(e => e.monthlyCost).reduce((sum, cost) => sum + cost);
}
get headCount() {return this.staff.length;}
}
“对于这个坏滋味,我想革新一下之前那个 Employee
和 Department
的例子来进行解说。”
“这个例子能够看到,我把 staff
字段从 Department
上移到了 Party
类,但其实 Employee
类并不关怀 staff
这个字段。这就是 被回绝的遗赠
坏滋味。”
“重构手法也很简略,就是把 staff
字段下移到真正须要它的子类 Department
中就能够了,就像我刚实现提炼超类那时的样子。”
“如果超类中的某个字段或函数只与一个或少数几个子类无关,那么最好将其从超类中挪走,放到真正关怀它的子类中去。”
“十有八九这种坏滋味很淡,须要对业务相熟程度较高能力发现。”
“咱们来看一下重构前后的比照。”
“那咱们持续下一个。”
正文(Comments)
“最初,再提一点,对于 正文
的坏滋味。”
“我认为,正文并不是坏滋味,并且属于一种好滋味,然而正文的问题在于很多人是常常把它当作“除臭剂”来应用。”
“你常常会看到,一段代码有着长长的正文,而后发现,这些正文之所以存在乃是因为代码很蹩脚,发明它的程序员不想管它了。”
“当你感觉须要写正文时,请先尝试重构,试着让所有正文都变得多余。”
“如果你不晓得该做什么,这才是正文的良好使用机会。除了用来记述未来的打算之外,正文还能够用来标记你并无十足把握的区域。你能够在正文里写下本人“为什么做某某事”。这类信息能够帮忙未来的批改者,尤其是那些健忘的家伙。”
小李小王疯狂拍板。
“好了,那咱们这次的特训就到此结束了,你们俩上来当前肯定要多多练习,造就辨认坏滋味的敏感度,而后做到对坏滋味的零容忍才行。”
小结
尽管题目是 24 大技巧,然而文章却介绍了 24 种代码里常见的坏滋味,还有每个坏滋味对应的重构手法。
这其实有点相似于设计准则和设计模式的关系,设计准则是道,设计模式是术。
有道者术能短暂,无道者术必落空,学术先需明道,方能大成,学术若不明道,终是小器。
这也是本文为什么要介绍 24 种代码里的坏滋味,而不是间接介绍重构手法。因为只有辨认了代码中的坏滋味,能力尽量避免写出坏滋味的代码,真正做到尽如人意,放弃软件衰弱长青。
如果发现了代码里的 坏滋味
,先把这片区域用 测试用例
圈起来,而后再利用 各种重构手法,在不扭转软件可察看行为的前提下,调整其构造
,在 通过测试
后,第一工夫 提交代码
,保障你的零碎随时都处于 可公布
状态。
文中的老耿原型其实就是《重构:改善既有代码的设计》的作者们,小王小李指的是团队中那些常常容易把代码写的像打补丁,而后过了一段时间老是想颠覆重来的编程新人们(也可能是老人),而大宝则像是一名手握屠龙术却不敢直面恶龙的高级工程师。
我认为,重构也须要勇气,开始尝试的勇气。
配套练习
我将文中所有的案例都整顿到了 github
上,每个坏滋味都有一个独立的目录,每个目录的构造看起来就像是这样。
xx.before.js
:重构前的代码xx.js
:重构后的代码xx.test.js
:配套的测试代码
强烈建议读者们依照文章教程,自行实现一次重构练习,这样能够更好的辨认坏滋味和把握重构手法。
上面是对应的链接:
- 神秘命名(Mysterious Name)
- 反复代码(Repeat Code)
- 过长函数(Long Function)
- 过长参数列表(Long Parameter List)
- 全局数据(Global Data)
- 可变数据(Mutable Data)
- 发散式变动(Divergent Change)
- 霰弹式批改(Shotgun Surgery)
- 依恋情节(Feature Envy)
- 数据泥团(Data Clumps)
- 根本类型偏执(Primitive Obsession)
- 反复的 switch(Repeated switch)
- 循环语句(Loop)
- 冗赘的元素(Lazy Element)
- 沉默寡言通用性(Speculative Generality)
- 长期字段(Temporary Field)
- 过长的音讯链(Message Chains)
- 中间人(Middle Man)
- 底细交易(Insider Trading)
- 过大的类(Large Class)
- 殊途同归的类(Alternative Classes with Different Interfaces)
- 纯数据类(Data Class)
- 被回绝的遗赠(Refuse Bequest)
- 正文(Comments)
最初一件事
如果您曾经看到这里了,心愿您还是点个赞再走吧~
您的点赞是对作者的最大激励,也能够让更多人看到本篇文章!
如果感觉本文对您有帮忙,请帮忙在 github 上点亮 star
激励一下吧!