在《重構(gòu)改善既有代碼設(shè)計》一書中,作者的經(jīng)驗是霞捡,重構(gòu)的大多數(shù)手法都是源自對于函數(shù)進(jìn)行的處理坐漏,絕大多數(shù)是從過長函數(shù)開始。結(jié)合我的實際處理情況碧信,不外如是仙畦。
過長函數(shù),確實很討厭音婶。因為他們往往向書中所說慨畸,會包含太多的信息,這些信息又會被函數(shù)錯綜復(fù)雜的邏輯衣式。不易鑒別寸士。如何對付過長函數(shù),作者系統(tǒng)化的總結(jié)出了一套經(jīng)驗:
對付過長函數(shù)碴卧,一項重要的重構(gòu)手法就是.Extract Method (110), 它把一段代碼從原先函 數(shù)中提取出來弱卡,放進(jìn)一個單獨函數(shù)中。而Inline Method(117)正好相反:將一個函數(shù)調(diào)用動作替換為該函數(shù)本體住册。如果在進(jìn)行多次提煉之后婶博,意識到提煉所得的某些函數(shù) 并沒有做任何實質(zhì)事情,或如果需要回溯恢復(fù)原先函數(shù)荧飞,我就需要Inline Method (117)凡人。
Extract Method (110)最大的困難就是處理局部變量,而臨時變量則是其中一個 主要的困難源頭叹阔。處理一個函數(shù)時挠轴,我喜歡運用Replace Temp with Query (120)去掉 所有可去掉的臨時變量。如果很多地方使用了某個臨時變量耳幢,我就會先運用既Split Temporary Variable (128)將它變得比較容易替換岸晦。
但有時候臨時變量實在太混亂,難以替換。這時候我就需要使用Replace Method with Method Object(135) 它讓我可以分解哪怕最混亂的函數(shù)启上,代價則是引入一個新
類邢隧。
注釋:重構(gòu)手法后面的括號是指書中的對應(yīng)處理手法的頁數(shù),之所以列出來也是為了讀者可以方便去書中直接找對應(yīng)的處理方法冈在。
下面會一一介紹包括使用邏輯以及注意點:
一府框、Extract Method (提煉函數(shù))
你有一段代碼可以被組織在一起并獨立出來。將這段代碼放進(jìn)一個獨立函數(shù)中讥邻,并讓函數(shù)名稱解釋該函數(shù)的用途迫靖。
為什么需要提煉函數(shù)呢? 首先兴使,如果每個函數(shù)的粒度都很小系宜,那么函數(shù)被復(fù)用的機會就更大;其次发魄,這會使高層函數(shù)讀起來就像一系列注釋盹牧;再次,如果函數(shù)都是細(xì)粒度励幼,那么函數(shù)的覆寫也會更容易些汰寓。
做法:
1、創(chuàng)造一個新函數(shù)苹粟,根據(jù)這個函數(shù)的意圖來對它命名(以它"做什么”來命名有滑, 而不是以它“怎樣做”命名)。
2嵌削、將提煉出的代碼從源函數(shù)復(fù)制到新建的目標(biāo)函數(shù)中毛好。
3、仔細(xì)檢査提煉出的代碼苛秕,看看其中是否引用了"作用域限于源函數(shù)”的變量
(包括局部變量和源函數(shù)參數(shù))肌访。
4、檢査被提煉代碼段艇劫,看看是否有任何局部變量的值被它改變吼驶。如果一個臨時 變量值被修改了,看看是否可以將被提煉代碼段處理為一個查詢店煞,并將結(jié)果值賦值給相關(guān)變量蟹演。
如果很難這樣做,或者如果被修改的變量不止一個浅缸,你就不能僅僅將這段代碼原封不動地提煉出來轨帜。你可能需要先使用Splite Temporary Variable (128),然后再嘗試提煉魄咕。也可以使用Temp with Query (120) 將臨時變量消滅掉衩椒。
5、將被提煉代碼段中需要讀取的局部變量,當(dāng)作參數(shù)傳給目標(biāo)函數(shù)毛萌。
6苟弛、處理完所有局部變量之后,進(jìn)行編譯阁将。
7膏秫、在源函數(shù)中,將被提煉代碼段替換為對目標(biāo)函數(shù)的調(diào)用做盅。
8缤削、如果你將任何臨時變量移到目標(biāo)函數(shù)中,請檢查它們原本的聲明式是否在被提煉代碼段的外圍吹榴。如果是亭敢,現(xiàn)在你可以刪除這些聲明式了。
9图筹、編譯帅刀,測試。
如果你發(fā)現(xiàn)源函數(shù)的參數(shù)被賦值远剩,應(yīng)該馬上使用Remove Assignments to Parameters (131)就是將入?yún)⒌木植孔兞吭儋x值給一個同類型的參數(shù)扣溺。
void method(Integer a, Integer b) {
// Remove Assignments to Parameters
Integer c = a;
c += 1;
}
特殊情況
被賦值的臨時變量也分兩種情況。較簡單的情況是:這個變量只在被提煉代碼段中使用瓜晤。果真如此锥余,你可以將這個臨時變量的聲明移到被提煉代碼段中,然后一 起提煉出去痢掠。另一種情況是:被提煉代碼段之外的代碼也使用了這個變量哈恰。這又分 為兩種情況:如果這個變量在被提煉代碼段之后未再被使用,你只需直接在目標(biāo)函數(shù)中修改它就可以了志群;如果被提煉代碼段之后的代碼還使用了這個變量着绷,你就需要讓目標(biāo)函數(shù)返回該變量改變后的值。
代碼示例:(帶有修改局部變量在賦值的情況)
void printowing() (
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner()锌云;
// calculate outstanding while (e.hasMoreElements()) (
Order each = (Order) e.nextElement();
outstanding += each.getAmount()荠医;
)
printDetails(outstanding);
)
現(xiàn)在我把“計算”代碼提煉出來:
void printowing() (
printBanner();
double outstanding = getOutstanding();
printDetails(outstanding)桑涎;
}
/** Enumeration變量e只在被提煉代碼段中用到彬向,所以可以將它整個搬到新函數(shù)
中。double變量outstanding在被提煉代碼段內(nèi)外都被用到攻冷,所以必須讓提煉出 來的新函數(shù)返回它娃胆。
*/
double getOutstanding() (
Enumeration e = _orders.elements();
double outstanding = 0.0;
while (e.hasMoreElements()) (
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
return outstanding等曼;
}
本例中的outstanding變量只是很單純地被初始化為一個明確初值里烦,所以我可 以只在新函數(shù)中對它初始化凿蒜。如果代碼還對這個變量做了其他處理,就必須將它的 值作為參數(shù)傳給目標(biāo)函數(shù)胁黑。
如果需要返回的變量不止一個废封,又該怎么辦呢???
有幾種選擇。最好的選擇通常是:挑選另一塊代碼來提煉丧蘸。我比較喜歡讓每個 函數(shù)都只返回一個值漂洋,所以會安排多個函數(shù),用以返回多個值力喷。如果你使用的語言 支持“出參數(shù)"(output parameter),可以使用它們帶回多個回傳值刽漂。但我還是盡可 能選擇單一返回值。
臨時變量往往為數(shù)眾多弟孟,甚至?xí)固釤捁ぷ髋e步維艱爽冕。這種情況下,我會嘗試 先運用Replace Temp with Query (120)減少臨時變量披蕉。如果即使這么做了提煉依舊困難重重颈畸,我就會動用Replace Method with Method Object (135),這個重構(gòu)手法不在乎 代碼中有多少臨時變量,"也不在乎你如何使用它們没讲。
二眯娱、Inline Method (內(nèi)聯(lián)函數(shù))
一個函數(shù)的本體與名稱同樣清楚易懂。在函數(shù)調(diào)用點插入函數(shù)本體爬凑,然后移除該函數(shù)徙缴。比起提煉函數(shù),內(nèi)聯(lián)函數(shù)的操作正好相反嘁信,
實例:
int getRatingf) {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() (
return _numberOfLateDeliveries > 5于样;
}
重構(gòu)后:
int getRating() {
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
本重構(gòu)方法好像很簡單,但是為了以防萬一導(dǎo)致出現(xiàn)其他問題潘靖,還是羅列出具體的操作動作穿剖。
做法:
1、檢査函數(shù)卦溢,確定它不具多態(tài)性糊余。如果子類繼承了這個函數(shù),就不要將此函數(shù)內(nèi)聯(lián)单寂,因為子類無法覆寫一個 根本不存在的函數(shù)贬芥。
2、找出這個函數(shù)的所有被調(diào)用點宣决。
3蘸劈、將這個函數(shù)的所有被調(diào)用點都替換為函數(shù)本體。
4尊沸、編譯威沫,測試贤惯。
5、刪除該函數(shù)的定義壹甥。
三救巷、Inline Temp (內(nèi)聯(lián)臨時變量)
你有一個臨時變量壶熏,只被一個簡單表達(dá)式賦值一次句柠,而它妨礙了其他重構(gòu)手法。
將所有對該變量的引用動作棒假,替換為對它賦值的那個表達(dá)式自身溯职。
示例:
double basePrice = anOrder.basePrice();
return (basePrice > 1000)
重構(gòu)為:
return (anOrder.basePrice() > 1000)
Inline Temp (119)多半是作為Replace Temp with Query (120)的一部分使用的,所 以真正的動機出現(xiàn)在后者那兒帽哑。唯一單獨使用Inline Temp情況是:你發(fā)現(xiàn)某 個臨時變量被賦予某個函數(shù)調(diào)用的返回值谜酒。
實現(xiàn)步驟:
1、檢査給臨時變量賦值的語句妻枕,確保等號右邊的表達(dá)式?jīng)]有副作用僻族。
2、如果這個臨時變量并未被聲明為final,那就將它聲明為final,然后編譯屡谐。=?這可以檢查該臨時變量是否真的只被賦值一次述么。
3、找到該臨時變量的所有引用點愕掏,將它們替換為“為臨時變量賦值”的表達(dá)式度秘。
4、每次修改后饵撑,編譯并測試剑梳。
5、修改完所有引用點之后滑潘,刪除該臨時變量的聲明和賦值語句垢乙。
6、編譯语卤,測試侨赡。
四、Replace Temp with Query (以查詢?nèi)〈R時變量)
你的程序以一個臨時變量保存某一表達(dá)式的運算結(jié)果粱侣。
將這個表達(dá)式提煉到一個獨立函數(shù)中羊壹。將這個臨時變量的所有引用點替換為對新函數(shù)的調(diào)用。此后齐婴,新函數(shù)就可被其他函數(shù)使用油猫。
double basePrice = .quantity * _itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;
重構(gòu)后:
if (basePrice() > 1000)
return basePrice() * 0.95;
else
return basePrice() * 0.98;
double basePrice() {
return _quantity * _itemPrice柠偶;
}
這樣實現(xiàn)的動機為:
臨時變量的問題在于:它們是暫時的情妖,而且只能在所屬函數(shù)內(nèi)使用睬关。由于臨時 變量只在所屬函數(shù)內(nèi)可見,所以它們會驅(qū)使你寫出更長的函數(shù)毡证,因為只有這樣你才 能訪問到需要的臨時變量电爹。如果把臨時變量替換為一個査詢,那么同一個類中的所 有函數(shù)都將可以獲得這份信息料睛。這將帶給你極大幫助丐箩,使你能夠為這個類編寫更清 晰的代碼。
這個重構(gòu)手法較為簡單的情況是:臨時變量只被賦值一次恤煞,或者賦值給臨時變 量的表達(dá)式不受其他條件影響屎勘。其他情況比較棘手,但也有可能發(fā)生居扒。你可能需要 先運用Splite Temporary Variable (128分解臨時變量為多個)或 Separate Query from Modifier (279將查詢函數(shù)和修改函數(shù)分離) 使情況變得簡單一些概漱,然后再替換臨時變量。如果你想替換的臨時變量是用來收集結(jié)果的(例如循環(huán)中的累加值)喜喂,就需要將某些程序邏輯(例如循環(huán))復(fù)制到查詢函數(shù)去瓤摧。
做法:
1、找出只被賦值一次的臨時變量玉吁。
2照弥、今如果某個臨時變量被賦值超過一次,考慮使用splite Temporary Variable 將它分割成多個變量诈茧。
3产喉、將該臨時變量聲明為final。-》確保改變量只被賦值一次
4敢会、編譯曾沈。
5、將"對該臨時變量賦值”之語句的等號右側(cè)部分提煉到一個獨立函數(shù)中鸥昏。
5.1 首先將函數(shù)聲明為private.日后你可能會發(fā)現(xiàn)有更多類需要使用它塞俱,那時放松對它的保護(hù)也很容易.
5.2 確保提煉出來的函數(shù)無任何副作用,也就是說該函數(shù)并不修改任何對象內(nèi) 容吏垮。如果它有副作用障涯,就對它進(jìn)行 Separate Query from Modifier (279).
6、編譯膳汪,測試唯蝶。
7、在該臨時變量身上實現(xiàn) Inline Temp 遗嗽。
我們常常使用臨時變量保存循環(huán)中的累加信息粘我。在這種情況下,整個循環(huán)都可 以被提煉為一個獨立函數(shù)痹换,這也使原本的函數(shù)可以少掉幾行擾人的循環(huán)邏輯征字。有時 候都弹,你可能會在一個循環(huán)中累加好幾個值,就像本書第26頁的例子那樣匙姜。這種情況下你應(yīng)該針對每個累加值重復(fù)一遍循環(huán)畅厢,這樣就可以將所有臨時變量都替換為查詢。
示例如下:
開始的實例:
首先氮昧,我從一個簡單函數(shù)開始:
double getPrice() (
int basePrice = _quantity * _itemPrice框杜;
double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor郭计;
}
我希望將兩個臨時變量都替換掉霸琴。
先把臨時變量聲明為final,檢查它們是否的確只被賦值一次.
double getPrice() {
final int basePrice = _quantity * _itemPrice;
final double discountFactor椒振;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor昭伸;
}
如果有任何問題,編譯器就會警告我澎迎。之所以先做這件事庐杨,因為如 果臨時變量不只被賦值一次,我就不該進(jìn)行這項重構(gòu)夹供。
重構(gòu)后:
private int basePrice() (
return _quantity * _itemPrice灵份;
}
private double discountFactor() (
if (basePrice() > 1000) return 0.95;
else return 0.98哮洽;
}
最終填渠,getPrice ()變成了這樣:
double getPrice() {
return basePrice() * discountFactor();
}
五鸟辅、 Introduce Explaining Variable (引入解釋性變量)
你有一個復(fù)雜的表達(dá)式氛什。將該復(fù)雜表達(dá)式(或其中一部分)的結(jié)果放進(jìn)一個臨時變量,以此變量名稱來解釋表達(dá)式用途匪凉。
if ( (platform. toUpperCase () . indexOf () > -1) && (browser.toUpperCase().indexOf(nIE") > -1) && waslnitialized() && resize > 0)
{
// do something
}
重構(gòu)后:
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1; final boolean isIEBrowser = browser. toUpperCase () . indexOf ('* IE") > -1; final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && waslnitialized().&& wasResized) (
//do something
}
動機:緣由
表達(dá)式有可能非常復(fù)雜而難以閱讀枪眉。這種情況下,臨時變量可以幫助你將表達(dá) 式分解為比較容易管理的形式再层。你可以用這項 重構(gòu)將每個條件子句提煉出來贸铜,以一個良好命名的臨時變量來解釋對應(yīng)條件子句的 意義。代碼可讀性強聂受。
做法
做法
1蒿秦、聲明一個final臨時變量,將待分解之復(fù)雜表達(dá)式中的一部分動作的運算結(jié)果賦值給它蛋济。
2棍鳖、將表達(dá)式中的“運算結(jié)果”這一部分,替換為上述臨時變量瘫俊。
3鹊杖、如果被替換的這一部分在代碼中重復(fù)出現(xiàn)悴灵,你可以毎次一個,逐一替換骂蓖。
4积瞒、編譯,測試登下。
5茫孔、重復(fù)上述過程,處理表達(dá)式的其他部分被芳。
示例
我們從一個簡單計算開始:
double price() (
// price is base price - quantity discount + shipping
return .quantity * _itemPrice
-Math.max(0, _quantity - 500) * _itemPrice * 0.05
+Math.min(_quantity * _itemPrice * 0.1, 100.0);
}
這段代碼還算簡單缰贝,不過我可以讓它變得更容易理解。首先我發(fā)現(xiàn)畔濒,底價(base price)等于數(shù)量(quantity)乘以單價(item price)o, 于是剩晴,我把這一部分計算的結(jié) 果放進(jìn)一個臨時變量中:
稍后也用上了 '‘?dāng)?shù)量乘以單價"運算結(jié)果,所以我同樣將它替換為basePrice 臨時變量.
批發(fā)折扣(quantity discount)的計算提煉出來侵状,將結(jié)果賦予臨時變量.
最后赞弥,我再把運費(shipping)計算提煉出來,將運算結(jié)果賦予臨時變量 shipping趣兄。同時我還可以刪掉代碼中的注釋绽左,因為現(xiàn)在代碼已經(jīng)可以完美表達(dá)自 己的意義了:
重構(gòu)后:
double price() (
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.rnax(Or _quantity - 500)* _itemPrice * 0.05; final double shipping = Math.min(basePrice * 0.1, 100.0);
return basePrice - quantityDiscount + shipping;
}
使用 Extract Method處理上述范例
面對上述代碼艇潭,我通常不會以臨時變量來解釋其動作意圖拼窥,我更喜歡使用 Extract Method(110)。
這一次我把底價計算提煉到一個獨立函數(shù)中蹋凝,批發(fā)折扣(quantity discount)的計算提煉出來鲁纠,運費(shipping)計算提煉出來。
最終可以得到如下的代碼:
double price() (
return basePrice() - quantityDiscount() + shipping()仙粱;
)
// 一開始我會把這些新函數(shù)聲明為private房交; 如果其他對象也需要它們,我可以輕易釋放這些函數(shù)的訪問限制伐割。
private double quantityDiscount() (
return Math.max(0, .quantity - 500) * _itemPrice * 0.05候味;
)
private double shipping() (
return Math.min(basePrice() * 0.1, 100.0);
}
private double basePrice() (
return ^quantity * _itemPrice隔心;
}
說明:那么白群,應(yīng)該在什么時候使用Introduce Explaining Variable (124)呢?答案是:在 Extract Method (110)需要花費更大工作量時硬霍。如果我要處理的是一個擁有大量局部變量的算法帜慢,那么使用Extract Method (110)絕非易事。這種情況下就會使用Introduce Explaining Variable (124)來理清代碼,然后再考慮下一步該怎么辦粱玲。搞清楚代碼邏 輯之后躬柬,我總是可以運用Replace Temp with Query (120)把中間引入的那些解釋性臨 時變量去掉。況且抽减,如果我最終使用Replace Method with Method Object (135)>那么 中間引入的那些解釋性臨時變量也有其價值允青。