為什么要重構(gòu)
- 重構(gòu)改進(jìn)軟件的設(shè)計
設(shè)計欠佳的程序往往需要更多的代碼吴超,重構(gòu)一個重要方向就是消除重復(fù)代碼
軟件變壞的途徑: 一個有架構(gòu)的軟件 > 修改代碼 > 沒有理解架構(gòu)設(shè)計 > 代碼沒有結(jié)構(gòu) > 修改代碼 > 難以讀懂原有設(shè)計 > 一個腐爛的架構(gòu)軟件
軟件變好的途徑: 一個腐爛的架構(gòu)軟件 > 修改代碼 > 改進(jìn)架構(gòu)設(shè)計 > 更具有結(jié)構(gòu) > 修改代碼 > 簡單易懂更易擴(kuò)展 > 一個好的架構(gòu)軟件
- 重構(gòu)使軟件更容易理解
編程的核心: 準(zhǔn)確說出我想要干什么汇恤,除了告訴計算機(jī)幢踏,還有其他的讀者
原來一個程序員要花一周時間來修改某段代碼搀矫,在重構(gòu)后更容易理解刑峡,現(xiàn)在只用花一小時就能搞定,這個就是時間成本阐污,人力成本休涤,軟件成本,公司成本的體現(xiàn)
- 重構(gòu)幫助找到bug
我不是一個特別好的程序員笛辟,我只是一個有著一些特別好的習(xí)慣的還不錯的程序員
特別好的程序員可以盯著一大段代碼可以找出bug, 我不行功氨,但是重構(gòu)了后序苏,代碼有了結(jié)構(gòu),脈絡(luò)疑故,bug會自動跑出來
- 重構(gòu)提高編程速度
我花在重構(gòu)上的時間杠览,難道不是在降低開發(fā)速度嗎?
但是纵势,經(jīng)常會聽到這樣的故事: 一開始進(jìn)展的很快踱阿,但如今想要添加一個新功能需要的時間越來越長,需要花很多時間想著怎么把新功能塞進(jìn)現(xiàn)有的代碼庫(最好的當(dāng)然不是塞進(jìn)钦铁,是放進(jìn))软舌, 不斷的有bug, 修復(fù)起來也越來越慢,不斷的給補丁打補丁牛曹,逐漸變成了一個考古工作者
何時重構(gòu)
三次法則: 第一次去做某件事盡管去做佛点,第二次做類似的事會有點反感,但是無論如何也要去做黎比,第三次再做類似的事超营,你就該重構(gòu)了。
- 預(yù)備性重構(gòu): 讓增加新功能更容易
增加新功能時阅虫,對老代碼的微調(diào)演闭,會使工作容易很多
例子: 增加一個功能時,發(fā)現(xiàn)有一個函數(shù)跟我功能很類似颓帝,但是里面幾個字段或者值不一樣米碰,如果不重構(gòu),你就會把代碼復(fù)制過來购城,修改幾個值吕座,這就導(dǎo)致重復(fù)代碼,將來修改代碼就要改兩次瘪板,如果重構(gòu)下老的函數(shù)吴趴,增加一個參數(shù),這樣就是預(yù)備性重構(gòu)
- 幫助理解的重構(gòu): 使代碼更易讀懂
要把腦子里的理解轉(zhuǎn)移到代碼本身侮攀,這份知識才保存的更久锣枝,同事也能看到
給一兩個變量改名,讓他們更清晰的表達(dá)意圖
一個長函數(shù)拆開幾個小函數(shù)魏身,更易理解
已經(jīng)理解了代碼意圖惊橱,但是邏輯過于迂回復(fù)雜,精簡下更好
有計劃的重構(gòu)和見機(jī)行事的重構(gòu)
上面兩個都是見機(jī)行事的重構(gòu)箭昵,但是當(dāng)功能增加到一定的時候税朴,簡單的重構(gòu)會有瓶頸,會發(fā)現(xiàn)一開始考慮不周的架構(gòu)設(shè)計,那么現(xiàn)在就需要有計劃的重構(gòu)長期重構(gòu)
但是很多重構(gòu)會花費幾個星期正林,幾個月的時間泡一,還有一大堆混亂的依賴關(guān)系,很多人參與觅廓,不可能停下來完全重構(gòu)鼻忠,那么可以每個人都達(dá)成共識,每天往想改進(jìn)的方向推動一點點杈绸,但是保持基本的功能不變帖蔓,比如要換掉一個庫,可以引入新的抽象瞳脓,兼容兩個庫的接口塑娇,等調(diào)用方慢慢切換過來,這樣換掉原來的庫就簡單多了代碼復(fù)審的時候重構(gòu)(code review)
很多時候自己看不出劫侧,或者經(jīng)驗不足埋酬,重構(gòu)后仍然不夠好,那么就需要有專門的code review, 來幫助我們更好的重構(gòu)代碼
何時不該重構(gòu)
- 看見一堆凌亂的代碼烧栋,但是我不需要修改的時候写妥,如果丑陋的代碼被隱藏在一個API下,就可以容忍它的丑陋审姓,等理解工作原理后珍特,再重構(gòu)
- 重寫比重構(gòu)還容易的,就別重構(gòu)了
怎么重構(gòu)
1. 命名規(guī)范
好的命名是整潔代碼的核心邑跪,使用范圍越廣的越要注意命名
來看一句神秘的代碼次坡,用一個變量表示高度呼猪,單位m
var height_rice = 4; // 高度為4米的變量, rice寫成米的英文
改變函數(shù)聲明
好辦法: 先寫一句注釋描述這個函數(shù)的作用画畅,再把這句注釋變成函數(shù)名字
function calc(height, width) {
return height * width;
}
function calcArea(height, width) {
return height * width;
}
變量改名
var a = height * width;
var area = height * width;
2. 重復(fù)代碼
如果在一個地方以上看到相同的代碼結(jié)構(gòu),就要設(shè)法將他們合二為一, 這個時候需要提煉函數(shù)來提供統(tǒng)一的使用方式:
提煉函數(shù)
什么時候把代碼放進(jìn)獨立的函數(shù): 將意圖與實現(xiàn)分開
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding();
//print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}
可以看到上面是想要打印日志
的意圖宋距,至于怎么打印則是實現(xiàn)轴踱,所以提取函數(shù)如下,至于命名谚赎,則是秉承次函數(shù)是 "做什么" 來命名:
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding();
printDetails(outstanding);
function printDetails(outstanding) {
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}
}
如果代碼是相似而不是完全相同淫僻,那么使用移動語句
來讓相關(guān)的代碼,結(jié)構(gòu)在一起壶唤,這是提煉函數(shù)的前提雳灵,別看這個很簡單, 很多的重構(gòu)都是從這里開始
移動語句
下面是一段計算商品訂單經(jīng)費的代碼闸盔,完全沒有分類悯辙,很難理解業(yè)務(wù)流程
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
const baseCharge = pricingPlan.base;
let charge;
const chargePerUnit = pricingPlan.unit;
const units = order.units;
let discount;
charge = baseCharge + units * chargePerUnit;
let discountableUnits = Math.max(units - pricingPlan.discountThreshold, 0);
discount = discountableUnits * pricingPlan.discountFactor;
if (order.isRepeat) discount += 20;
charge = charge - discount;
chargeOrder(charge);
采用移動語句之后,把相同的功能移動到一起分類,流程清晰躲撰,之后才能提取函數(shù)來進(jìn)一步重構(gòu)代碼
// 報價計劃
const pricingPlan = retrievePricingPlan();
const baseCharge = pricingPlan.base;
const chargePerUnit = pricingPlan.unit;
// 訂單數(shù)量
const order = retreiveOrder();
const units = order.units;
// 折扣
let discount;
let discountableUnits = Math.max(units - pricingPlan.discountThreshold, 0);
discount = discountableUnits * pricingPlan.discountFactor;
// 具體經(jīng)費
let charge;
if (order.isRepeat) discount += 20;
charge = baseCharge + units * chargePerUnit;
charge = charge - discount;
chargeOrder(charge);
函數(shù)上移
如果重復(fù)代碼位于繼承的子類中的時候针贬,可以把相同的代碼提到父類,避免子類之間互相調(diào)用
class Employee {...}
class Salesman extends Employee {
get name() {...}
}
class Engineer extends Employee {
get name() {...}
}
可以看到上訴子類都有相同的name()方法拢蛋,可以把方法上移到父類中
class Employee {
get name() {...}
}
class Salesman extends Employee {...}
class Engineer extends Employee {...}
3. 過長的函數(shù)
老程序員的經(jīng)驗: 活的最長桦他,最好的程序,其中的函數(shù)都比較短谆棱,函數(shù)越長快压,越難理解,小函數(shù)易于理解的關(guān)鍵還是在于良好的命名垃瞧,好的命名就能讓人了解函數(shù)的作用嗓节,可以參考我的一個原則: 每當(dāng)感覺需要以注釋來說明點什么的時候,我們就需要把說明的東西寫進(jìn)一個獨立的函數(shù)里,并以其用途(而非實現(xiàn)手法)命名皆警, 一定要注意函數(shù) "做什么" 和 “怎么做”之間的語義理解拦宣,掌握了這點,就掌握了函數(shù)用法的精髓信姓。
在把長函數(shù)分解成小函數(shù)過程中鸵隧,常常會遇到函數(shù)內(nèi)有大量的參數(shù)和臨時變量,如果你只是提取函數(shù)意推,就會把許多參數(shù)傳遞給被提煉的函數(shù)豆瘫,從可讀性上面來說沒有任何提升
以查詢?nèi)〈R時變量
const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;
上面生成了臨時變量basePrice
, 完全可以放到類屬性里面,這樣在提取函數(shù)的時候菊值,就少了一個臨時變量外驱,不用當(dāng)成參數(shù)傳遞了
class Price {
get basePrice() {this._quantity * this._itemPrice;}
}
...
if (this.basePrice > 1000)
return this.basePrice * 0.95;
else
return this.basePrice * 0.98;
引入?yún)?shù)對象
對于過長的參數(shù)列表,引入?yún)?shù)對象是個好辦法腻窒,這樣可以簡化為一個參數(shù)結(jié)構(gòu)
function amountInvoiced(startDate, endDate) {...}
function amountReceived(startDate, endDate) {...}
function amountOverdue(startDate, endDate) {...}
上面代碼每個函數(shù)都在傳遞三個時間參數(shù)昵宇,就可以提煉一個時間的數(shù)據(jù)類來統(tǒng)一管理
class DateRange {
string startDate;
string middleDate
string endDate;
}
function amountInvoiced(dateRange) {...}
function amountReceived(dateRange) {...}
function amountOverdue(dateRange) {...}
劃重點
:這項重構(gòu)方法具有更深層的改變 *新的數(shù)據(jù)結(jié)構(gòu) -> 重組函數(shù)來使用新結(jié)構(gòu) -> 捕捉圍繞新數(shù)據(jù)結(jié)構(gòu)的公用函數(shù) -> 構(gòu)建新的類來組合新的數(shù)據(jù)結(jié)構(gòu)和函數(shù) -> 形成新的抽象概念 -> 改變整個軟件架構(gòu)圖景, 所以說儿子,新結(jié)構(gòu)的一小步才會有軟件架構(gòu)的一大步
函數(shù)組合成類
當(dāng)分成獨立的函數(shù)之后瓦哎,這不是代碼的終點,如果發(fā)現(xiàn)一組函數(shù)形影不離的操作著同一塊數(shù)據(jù)(做為參數(shù)傳給函數(shù))柔逼,此時就是時候組建一個類了
例如上面引入?yún)?shù)對象后的函數(shù)和數(shù)據(jù)結(jié)構(gòu)組合如下
class Amount { // 金額類
DateRange dateRange; // 時間范圍字段
Invoiced() {...}; // 發(fā)票金額方法
received() {...}; // 收支金額方法
overdue() {...}; // 欠款金額方法
}
使用類的好處:當(dāng)修改上面Amount類的dateRange這類核心數(shù)據(jù)時蒋譬,依賴于此的數(shù)據(jù),比如發(fā)票愉适,收支犯助,欠款等會與核心數(shù)據(jù)保持一致
4. 簡化條件邏輯
分解條件表達(dá)式
復(fù)雜的條件邏輯是最常導(dǎo)致復(fù)雜度上升的地方之一,所以適當(dāng)?shù)姆纸馑麄兛梢愿宄谋砻髅總€分支的作用
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
charge = quantity * plan.summerRate;
else
charge = quantity * plan.regularRate + plan.regularServiceCharge;
上面代碼很難直觀看出此條件是什么作用维咸,把這些條件和實現(xiàn)提取為函數(shù)后就非常清晰了剂买,夏天時候的支出和其他季節(jié)的支出不同
if (summer())
charge = summerCharge();
else
charge = regularCharge();
合并條件表達(dá)式
有時候發(fā)現(xiàn)一串條件檢查:檢查條件各不相同扑媚,最終行為卻一致,這種情況可以使用‘邏輯或‘ 或‘邏輯與’合并為一個條件表達(dá)式
if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
上面都是返回0的情況雷恃,就可以提煉為一個函數(shù)統(tǒng)一返回
if (isNotEligibleForDisability()) return 0;
function isNotEligibleForDisability() {
return ((anEmployee.seniority < 2)
|| (anEmployee.monthsDisabled > 12)
|| (anEmployee.isPartTime));
}
簡化嵌套條件表達(dá)式
條件表達(dá)式通常有兩種風(fēng)格疆股,第一種:兩個條件分支都屬于正常行為,這個時候可以用 if...else...的條件表達(dá)式倒槐;第二種: 只有一個條件分支是正常行為旬痹,另一個則是異常行為,發(fā)生情況很罕見讨越,此時應(yīng)該單獨檢查該條件两残,改條件為真時立即返回
function getPayAmount() {
let result;
if (isDead)
result = deadAmount();
else {
if (isSeparated)
result = separatedAmount();
else {
if (isRetired)
result = retiredAmount();
else
result = normalPayAmount();
}
}
return result;
}
上面代碼是一段根據(jù)不同員工狀態(tài)發(fā)工資的邏輯,死了有撫恤金把跨,辭退的有補償金人弓,退休了有退休金,平常就正常發(fā)工資着逐,很顯然崔赌,正常發(fā)工資是大概率事件,其他的都可以簡化為獨立判斷語句然后返回耸别,這樣代碼清晰
function getPayAmount() {
if (isDead) return deadAmount();
if (isSeparated) return separatedAmount();
if (isRetired) return retiredAmount();
return normalPayAmount();
}
以多態(tài)取代條件表達(dá)式
復(fù)雜的條件邏輯是編程中最難理解的東西健芭,多態(tài)是面向?qū)ο缶幊痰年P(guān)鍵特征之一,大部分簡單的條件判斷用if...else..或者switch...case...無關(guān)緊要秀姐,但是如果有四五個或更多的復(fù)雜條件邏輯慈迈,多態(tài)是改善這種情況的有力工具
function plumage(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return "average";
case 'AfricanSwallow':
return (bird.numberOfCoconuts > 2) ? "tired" : "average";
case 'NorwegianBlueParrot':
return (bird.voltage > 100) ? "scorched" : "beautiful";
default:
return "unknown";
}
把具體的實現(xiàn)封裝到類里方法,你可能會問省有,這不是還有switch和case嗎痒留?注意上面只是一個獲取羽毛的方法里用了swtich和case,如果以后我們又要根據(jù)鳥的種類獲取鳥的大小蠢沿,壽命等情況呢伸头,又要在很多方法里用這些討厭的swtich..case, 但是把他們用多態(tài)抽象為類后,可以像下面使用類似構(gòu)造工廠的方式來創(chuàng)建不同品種的鳥搏予,他們的接口都相同熊锭,后面只管調(diào)用了弧轧,往深處說雪侥,可以繼續(xù)用抽象工廠,或者控制反轉(zhuǎn)(IOC)等特性(VanGo平臺底層實現(xiàn)的精髓 ' . ' )徹底干掉這些swtich...case來實現(xiàn)動態(tài)創(chuàng)建對象精绎,當(dāng)然這些深入的東西這里就不討論了速缨。
function createBird(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return new EuropeanSwallow(bird);
case 'AfricanSwallow':
return new AfricanSwallow(bird);
case 'NorweigianBlueParrot':
return new NorwegianBlueParrot(bird);
default:
return new Bird(bird);
}
}
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";
}
5. 可變數(shù)據(jù)
對數(shù)據(jù)的經(jīng)常修改是導(dǎo)致出乎意料的結(jié)果和難以發(fā)現(xiàn)的bug, 我在一處更新了數(shù)據(jù),沒有意識到另一處用期望著完全不同的數(shù)據(jù)代乃,我們要約束數(shù)據(jù)更新
封裝變量
一個好的習(xí)慣: 對于所有可變數(shù)據(jù)旬牲,只要它的作用域超出了單個函數(shù)仿粹,我就會將其封裝起來,只允許通過函數(shù)訪問原茅,數(shù)據(jù)的作用域越大吭历,封裝就越重要
let defaultOwner = {firstName: "Martin", lastName: "Fowler"};
每次獲取或者設(shè)置值的時候通過函數(shù),可以監(jiān)控或者統(tǒng)一修改內(nèi)部來改變真正的值擂橘,避免了很多bug
let defaultOwnerData = {firstName: "Martin", lastName: "Fowler"};
export function defaultOwner() {return defaultOwnerData;}
export function setDefaultOwner(arg) {defaultOwnerData = arg;}
拆分變量
變量有各種不同的用途晌区,要避免臨時變量被多次賦值,如果變量承擔(dān)多個責(zé)任通贞,就應(yīng)該被分解為多個有獨立意義的變量
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);
將查詢函數(shù)和修改函數(shù)分離
如果函數(shù)只提供一個值朗若,沒有任何看得到的副作用,證明是個好函數(shù)昌罩,一個好的規(guī)則是: 任何有返回值的函數(shù)哭懈,都不應(yīng)該有看的見的副作用,如果遇到一個 “既有返回值又有副作用” 的函數(shù)茎用,證明這里會有“看不見的”可變數(shù)據(jù)遣总,就要試著將他們分離
function getTotalOutstandingAndSendBill() {
const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
sendBill();
return result;
}
可以看到在上面get函數(shù)里,sendBill()和這個函數(shù)沒有任何關(guān)系轨功,這就是副作用彤避,此時就要將它分離出來,這是要保證函數(shù)的純凈夯辖,所謂的“純函數(shù)”琉预,只有職責(zé)分離,才能干大事
function totalOutstanding() {
return customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {
emailGateway.send(formatBill(customer));
}
6. 繼承關(guān)系
子類父類功能隔離
比如一些子類公用函數(shù)蒿褂,字段就要函數(shù)上移
或者字段上移
到父類來統(tǒng)一管理圆米,相反如果是子類特有的函數(shù),字段啄栓,就要用函數(shù)下移
或者字段下移
到子類分別實現(xiàn)娄帖,這里就不寫具體例子了,希望讀者可以自行領(lǐng)會
提煉超類
一般的面向?qū)ο蟮乃枷胧牵?code>繼承必須是真實的分類對象模型的繼承昙楚,比如鴨子繼承動物近速;但是更實用的方法是: 發(fā)現(xiàn)一些共同的元素
,就把他們抽取到一起堪旧,于是有了繼承關(guān)系.
class Department {
get totalAnnualCost() {...}
get name() {...}
get headCount() {...}
}
class Employee {
get annualCost() {...}
get name() {...}
get id() {...}
}
上面部門和職員都有名字和年成本這兩個屬性削葱,那么我們把他們提到一個超類中,名叫組織淳梦,也有名字析砸,和年成本,這樣子類部門和職員可以通過覆蓋實現(xiàn)自己的年成本計算爆袍,同時他們公司名字可能相同的首繁,就復(fù)用父類代碼
class Party {
get name() {...}
get annualCost() {...}
}
class Department extends Party {
get annualCost() {...}
get headCount() {...}
}
class Employee extends Party {
get annualCost() {...}
get id() {...}
}
以委托取代子類
繼承是根據(jù)分類
用于把屬于某一類
公共的數(shù)據(jù)和行為放到超類中作郭,每個子類根據(jù)需求覆寫部分屬性,這是繼承的本質(zhì)弦疮,但是由于這種本質(zhì)體系夹攒,體現(xiàn)了他的缺點:繼承只能處理一個分類
方向上面的變化,但是子類上導(dǎo)致行為不同的原因有很多種
, 比如人我根據(jù)'年齡'來繼承分類胁塞,分為‘年輕人’和'老人'芹助,但是對于'富人'和'窮人'這個分類來看,其實相同年齡的'年輕人'行為是很不同的闲先,你們說是吧
class Order {
get daysToShip() {
return this._warehouse.daysToShip;
}
}
class PriorityOrder extends Order {
get daysToShip() {
return this._priorityPlan.daysToShip;
}
}
把繼承的寫法状土,提到超類的委托里面,這樣就是組合伺糠,所謂“對象組合優(yōu)于類繼承”也是這個道理蒙谓,一個原則是,先用繼承解決代碼復(fù)用問題训桶,發(fā)現(xiàn)分類不對了累驮,再改為委托
class Order {
get daysToShip() {
return (this._priorityDelegate)
? this._priorityDelegate.daysToShip
: this._warehouse.daysToShip;
}
}
class PriorityOrderDelegate {
get daysToShip() {
return this._priorityPlan.daysToShip
}
}
以委托取代超類
如果超類的一些函數(shù)對于子類并不適合,就說明我們不應(yīng)該通過繼承來獲得超類的功能舵揭,而改為委托谤专,合理的繼承關(guān)系有一個重要特征: 子類的所有實例都應(yīng)該是超類的實例,通過超類的接口來使用子類的實例應(yīng)該完全不出問題
class List {...}
class Stack extends List {...}
比如我們實現(xiàn)的棧類(Stack)原本繼承了列表類(List)午绳,但是發(fā)現(xiàn)很多列表的方法不適合棧置侍,那就改用委托(組合)關(guān)系來把列表當(dāng)成一個屬性放在子類中,然后封裝需要用到列表類的方法即可
class Stack {
constructor() {
this._storage = new List();
}
}
class List {...}