一,重構(gòu)熊咽,第一個案例
這一章作者先用一個影片出租程序的案例,來演示重構(gòu)的過程
public class Movie {
public static final int CHILDRENs = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String title;
private int priceCode;
public Movie(String title, int priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int priceCode) {
this.priceCode = priceCode;
}
}
public class Rental {
private Movie movie;
private int daysRented;
public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}
public Movie getMovie() {
return movie;
}
public void setMovie(Movie movie) {
this.movie = movie;
}
public int getDaysRented() {
return daysRented;
}
public void setDaysRented(int daysRented) {
this.daysRented = daysRented;
}
}
public class Customer {
private String name;
private Vector<Rental> _rentals = new Vector<>();
Customer(String name) {
this.name = name;
}
void addRental(Rental arg) {
_rentals.addElement(arg);
}
private String getName() {
return name;
}
String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration<Rental> rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = rentals.nextElement();
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount += 2;
if (each.getDaysRented() > 2)
thisAmount += (each.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENs:
thisAmount += 1.5;
if (each.getDaysRented()>3)
thisAmount += (each.getDaysRented() - 3) * 1.5;
break;
}
frequentRenterPoints++;
if (each.getMovie().getPriceCode()==Movie.NEW_RELEASE
&& each.getDaysRented() > 1)
frequentRenterPoints++;
result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
return result;
}
}
每個Customer顧客可以租多部Movie影片诅愚,程序會根據(jù)影片的不同類型和租賃天數(shù)酝锅,計算出租賃費用和獲得的積分到涂。
這個例子我先嘗試自己重構(gòu)了一遍涤妒,然后看完第一章再重構(gòu)了一遍她紫。
看完整本書以后,把代碼還原成最開始的樣子渐逃,自己再重構(gòu)了兩遍茄菊。
以下是我最后一次重構(gòu)的步驟:
- 先加單元測試面殖,根據(jù)影片類型的不同,以及租賃天數(shù)的臨界值相叁,增加多個test case增淹。
- while循環(huán)改為for循環(huán)埠通,變量each改名叫rental。
- 計算每個電影frequentRenterPoints的邏輯虽画,抽取方法getFrequentRenterPoints(Rental rental)码撰。
- 發(fā)現(xiàn)getFrequentRenterPoints(Rental rental)只依賴于Rental類,把方法移至Rental類个盆。
- 發(fā)現(xiàn)getFrequentRenterPoints()使用了movie.getPriceCode()颊亮,Rental應(yīng)該調(diào)用Movie的接口,而不是直接操作Movie的屬性绍在。把方法移至Movie變成getFrequentRenterPoints(int daysRented)偿渡。
- 消除getFrequentRenterPoints(int daysRented)中的臨時變量溜宽。
- 計算每個電影費用的邏輯适揉,抽取方法getCharge(Rental rental)涡扼。
- 發(fā)現(xiàn)getCharge(Rental rental)只依賴于Rental類吃沪,把方法移至Rental類。
- 再次發(fā)現(xiàn)getCharge()使用了movie.getPriceCode()红淡,把方法移至Movie變成getCharge(int daysRented)。
- 消除getCharge(int daysRented)中的臨時變量桶蝎。
- 用多態(tài)替換Switch登渣,如果創(chuàng)建三個子類繼承Movie胜茧,調(diào)用方就必須創(chuàng)建具體的子類對象(違反依賴倒置原則)呻顽。
一個對象具有狀態(tài)廊遍,并且不同狀態(tài)下有不同的行為昧碉,引入State模式:
創(chuàng)建接口Price作為Movie的屬性被饿,接口方法getCharge(int daysRented)狭握,再創(chuàng)建三個實現(xiàn)類论颅,把Switch分支的邏輯移至具體的實現(xiàn)類恃疯。 - 把getFrequentRenterPoints(int daysRented)也移至Price今妄。
- 修改Movie的構(gòu)造方法盾鳞,根據(jù)priceCode初始化新加的屬性對象price腾仅,去掉原有的priceCode屬性推励。
- 回到Customer验辞,statement()的循環(huán)中做了不止一件事:計算費用受神,計算積分鼻听,計算總費用撑碴,分開三個循環(huán)醉拓,分別抽方法亿卤。
以下是重構(gòu)以后的UML圖:
做了幾點總結(jié):
1 . 每個方法只做一件事,每個方法抽象層級不能多于兩層钻哩,根據(jù)這個原則抽取方法街氢。
2 . 根據(jù)類的職責(zé)和對象之間的依賴關(guān)系珊肃,把方法移至對應(yīng)的類近范。
3 . 應(yīng)該調(diào)用對象的接口方法评矩,不要直接操作對象的屬性斥杜。
4 . 盡量減少方法中的臨時變量蔗喂,簡化邏輯缰儿,增加可讀性乖阵,例如
public int getFrequentRenterPoints() {
int frequent = 1;
if (getMovie().getPriceCode() == Movie.NEW_RELEASE
&& daysRented > 1)
frequent++;
return frequent;
}
應(yīng)該改成
public int getFrequentRenterPoints() {
if (getMovie().getPriceCode() == Movie.NEW_RELEASE
&& daysRented > 1)
return 2;
return 1;
}
5 . 依賴倒置原則儒将,調(diào)用方應(yīng)該依賴抽象類或接口钩蚊,不要依賴具體實現(xiàn)類砰逻。
6 . 狀態(tài)模式 OR 工廠模式诱渤?
這塊暫時沒有太明白勺美,什么情況該用哪種模式缎脾。
狀態(tài)模式適用于有狀態(tài)的對象遗菠,在不同狀態(tài)下有不同的處理邏輯的情況辙纬。
這個案例感覺確實更適合用狀態(tài)模式贺拣。
工廠模式似乎更適用于創(chuàng)建各種對象譬涡,例如我自己第一次重構(gòu)時增加了PriceCalculator的概念,而案例中并沒有這個概念陨瘩。
7 . Code Kata舌劳,重構(gòu)的手法需要反復(fù)針對練習(xí),才能熟練掌握崇决。
二恒傻、重構(gòu)原則
1 . 重構(gòu)的定義
- (名詞形式)對軟件內(nèi)部結(jié)構(gòu)的一種調(diào)整,目的是在不改變軟件可察行為的前提下沸手,提高可理解性契吉,降低修改成本捐晶。
- (動詞形式)使用一些列重構(gòu)手法惑灵,在不改變軟件可觀察行為的前提下英支,調(diào)整其結(jié)構(gòu)潭辈。
2 . 軟件開發(fā)的兩頂帽子
- 添加新功能時把敢,不應(yīng)該修改既有代碼婶恼,只管添加新功能并通過測試勾邦。
- 重構(gòu)時不再添加新功能眷篇,只管改進程序結(jié)構(gòu)蕉饼,并通過已有測試。
3 . 為何重構(gòu)
- 重構(gòu)改進軟件設(shè)計(Design)
- 重構(gòu)使軟件更容易理解(Maintain)
- 重構(gòu)幫助找到BUG(Debug)
- 重構(gòu)提高編程速度(Efficiency)
4 . 何時重構(gòu)
- 三次法則:事不過三创肥,三則重構(gòu)
- 添加功能時重構(gòu)(New Feature)
- 修補錯誤時重構(gòu)(Bug Fix)
- 復(fù)審代碼時重構(gòu)(Code Review)
5 . 何時不該重構(gòu)
- 既有代碼太混亂叹侄,且不能正常工作塔猾,需要重寫而不是重構(gòu)丈甸。
- 項目接近最后期限時睦擂,應(yīng)該避免重構(gòu)顿仇。
6 . 重構(gòu)的目標(biāo)
為什么程序如此難以相與? | 設(shè)計與重構(gòu)的目標(biāo) |
---|---|
難以閱讀的程序述呐,難以修改 | 容易閱讀 |
邏輯重復(fù)的程序乓搬,難以修改 | 所有邏輯都只在唯一地點指定 |
添加新行為時需要修改已有代碼的程序,難以修改 | 新的改動不會危及現(xiàn)有行為 |
帶復(fù)雜條件邏輯的程序江掩,難以修改 | 盡可能簡單表達條件邏輯 |
7 . 間接層和重構(gòu)
-
間接層的價值
允許邏輯共享(避免重復(fù)代碼)
分開解釋意圖和實現(xiàn)(方法越短小环形,越容易起好名字揭示意圖着降,單一職責(zé))
隔離變化(軟件需要根據(jù)需求的變化不斷修改,隔離縮小修改的范圍)
封裝條件邏輯(多態(tài)消息) -
大多數(shù)重構(gòu)都為程序引入了更多的間接層
“計算機科學(xué)是這樣一門科學(xué):它相信所有問題都可以通過增加一個間接層來解決发侵。” ——Dennis Debruler -
過多的間接層會導(dǎo)致代碼的層次太深
使代碼難以閱讀.因此要權(quán)衡加入間接層的利弊
三叔锐、代碼的壞味道
我覺得這一章的內(nèi)容非常重要愉烙,識別出代碼的壞味道,是正確重構(gòu)的前提蔓肯。
1 . Duplicated Code(重復(fù)代碼)
代碼有很多中壞味道蔗包,重復(fù)是最壞的一種。
一個類中的兩個方法有重復(fù)代碼旧噪,可以通過抽取方法將重復(fù)代碼放到另一個方法中以供調(diào)用淘钟;
互為兄弟的子類中如果有重復(fù)代碼,可以將重復(fù)代碼抽取到父類中铁瞒;
兩個沒有關(guān)系的類中如果有重復(fù)代碼身辨,可以重新抽取一個類將重復(fù)代碼放到這個第三方類中煌珊。
2 . Long Method(過長函數(shù))
使用短小的方法首先符合高內(nèi)聚的要求,同時給方法起一個好的名字蔬浙,可以幫助理解方法的作用敛滋。
如果感覺方法的某個地方需要注釋來加以說明,可以把這部分代碼放入一個獨立的方法中庶艾,并以用途(而不是實現(xiàn)手法)來命名方法。
3 . Large Class(過大的類)
類的設(shè)計應(yīng)當(dāng)遵循單一職責(zé)原則(SRP)煤裙。重構(gòu)一個巨大的類可以使用抽取接口的方式來搞清楚這個類應(yīng)該如何分解硼砰。
4 . Long Parameter List(過長參數(shù)列表)
比較常見的是將相關(guān)的參數(shù)組織成一個對象來替換掉這些參數(shù)。
5 . Divergent Change(發(fā)散式變化)
因為不同的原因豹障,在不同的方向上,修改同一個類累魔。應(yīng)該分解成更小的類薛夜,每個類只因一種原因而修改。
這個深有體會版述,多層結(jié)構(gòu)系統(tǒng)梯澜,開發(fā)人員往往容易把全部邏輯都放在Service層,導(dǎo)致Service類非常龐大且不斷被修改渴析。
6 . Shotgun Surgery(霰彈式修改)
每遇到變化晚伙,需要修改多個類俭茧,容易遺漏咆疗。應(yīng)該把需要修改的部分放到一個類里。
7 . Feature Envy(依戀情結(jié))
函數(shù)大量地使用了另外類的數(shù)據(jù)母债。這種情況下最好將此函數(shù)移動到那個類中
8 . Data Clumps(數(shù)據(jù)泥團)
兩個類中相同的字段午磁、函數(shù)簽名中相同的參數(shù)等,都適合提取成一個單獨的數(shù)據(jù)類
9 . Primitive Obsession(基本類型偏執(zhí))
- 如果你有大量的基本數(shù)據(jù)類型字段毡们,就有可能將其中部分存在邏輯聯(lián)系的字段組織起來迅皇,形成一個類。更進一步的是衙熔,將與這些數(shù)據(jù)有關(guān)聯(lián)的方法也一并移入類中登颓。為了實現(xiàn)這個目標(biāo),可以嘗試 以類取代類型碼(Replace Type Code with Class) 红氯。
- 如果基本數(shù)據(jù)類型字段的值是用于方法的參數(shù)框咙,可以使用 引入?yún)?shù)對象(Introduce Parameter Object) 或 保持對象完整(Preserve Whole Object) 。
- 如果想要替換的數(shù)據(jù)值是類型碼痢甘,而它并不影響行為喇嘱,則可以運用 以類取代類型碼(Replace Type Code with Class) 將它替換掉。如果你有與類型碼相關(guān)的條件表達式塞栅,可運用 以子類取代類型碼(Replace Type Code with Subclass) 或 以狀態(tài)/策略模式取代類型碼(Replace Type Code with State/Strategy) 加以處理婉称。
- 如果你發(fā)現(xiàn)自己正從數(shù)組中挑選數(shù)據(jù),可運用 以對象取代數(shù)組(Replace Array with Object) 构蹬。
10 . Switch Statements(switch驚悚現(xiàn)身)
先將switch語句提煉成獨立的函數(shù)王暗,然后再將此函數(shù)搬移到需要多態(tài)的類里。
11 . Parallel Inheritance(平行繼承體系)
是Shortgun Surgery的一種特殊情況庄敛,每當(dāng)為某個類增加一個子類俗壹,必須也為另外一個類相應(yīng)增加一個子類。例如
蠟筆有大中小三種型號藻烤,12種顏色绷雏,總共必須有36種蠟筆头滔。
每增加一種顏色,都必須增加大中小三種型號涎显。顏色和型號緊緊耦合在一起坤检。
再來看毛筆,不同的毛筆型號抽象成毛筆期吓,不同顏色抽象成顏料早歇,
毛筆和顏料兩個基類形成關(guān)聯(lián),避免了Shortgun Surgery讨勤,這就是Bridge模式箭跳。
12 . Lazy Class(冗贅類)
如果一個類不值得存在,那么它就應(yīng)該消失潭千。
13 . Speculative Generality(夸夸其談未來性)
如果你的抽象類谱姓、委托、方法的參數(shù)沒有實際的作用刨晴,那么就應(yīng)當(dāng)被移除掉屉来。
14 . Temporary Field(令人迷惑的暫時字段)
類中某個字段只為某些特殊情況而設(shè)置。
15 . Message Chains(過度耦合的消息鏈)
常常是因為數(shù)據(jù)結(jié)構(gòu)的層次很深狈癞,需要層層調(diào)用getter獲取內(nèi)層數(shù)據(jù)奶躯。個人認為Message Chains如果頻繁出現(xiàn),考慮這個字段是否應(yīng)該移到較外層的類亿驾,或者把調(diào)用鏈封裝在較外層類的方法嘹黔。
16 . Middle Man(中間人)
如果一個類的很多功能都通過委托給其他類來完成,那么就不如去掉這些中間人直接和真正負責(zé)的對象打交道莫瞬。
17 . Inappropriate Intimacy(兩個類過度親密)
- 將雙向關(guān)聯(lián)改為單向關(guān)聯(lián)(Change Bidirectional Association to Unidirectional)儡蔓。
- 提煉類,將兩個類的共同點提煉到新類中疼邀,讓它們共同使用新類喂江。
- 繼承往往造成過度親密,運用以委托取代繼承(Replace Inheritance with Delegate)旁振。
18 . Alternative Classes(異曲同工的類)
19 . Incomplete Lib Class(不完美的類庫)
修改類庫的一兩個函數(shù) - 引入外部函數(shù)(Introduce Foreign Method)
添加一大堆額外行為 - 添加本地擴展(Introduce Local Extension)
20 . Data Class(純稚的數(shù)據(jù)類)
數(shù)據(jù)類不應(yīng)該把全部字段單純的通過getter/setter暴露出來(我們在多層結(jié)構(gòu)系統(tǒng)開發(fā)時經(jīng)常這么做)获询,
而應(yīng)該暴露抽象接口,封裝內(nèi)部結(jié)構(gòu)拐袜。
《Clean Code》第六章開始也有講過同樣的問題吉嚣。
21 . Refused Bequest(被拒絕的遺贈)
- 子類繼承父類的所有函數(shù)和數(shù)據(jù),子類只挑選幾樣來使用蹬铺。
為子類新建一個兄弟類尝哆,再運用下移方法(Push Down Method)和下移字段(Push Down Field)把用不到的函數(shù)下推個兄弟類。 - 子類只復(fù)用了父類的行為甜攀,卻不想支持父類的接口秋泄。
運用委托替代繼承(Replace Inheritance with Delegation)來達到目的琐馆。
22 . Comments(過多的注釋)
注釋不是用來補救劣質(zhì)代碼的,事實上如果我們?nèi)コ舜a中的所有壞味道恒序,當(dāng)劣質(zhì)代碼都被移除的時候瘦麸,注釋已經(jīng)變得多余,因為代碼已經(jīng)講清楚了一切歧胁。
四滋饲、構(gòu)筑測試體系
測試是安全重構(gòu)的前提。在項目開發(fā)工作中与帆,我們要求添加新代碼必須有單元測試,重構(gòu)舊代碼也要先加單元測試墨榄。
五玄糟、重構(gòu)列表
Java開發(fā),由于IDE(Intellij Idea)能夠很好的支持大多數(shù)情況下的重構(gòu)袄秩,有各種的自動提示阵翎,所以感覺暫時不需要用到重構(gòu)列表。
六之剧、重新組織函數(shù)
1 . Extract Method 提煉函數(shù)
將一段代碼放進一個獨立函數(shù)中郭卫,并讓函數(shù)名稱解釋該函數(shù)的用途。
增加可讀性背稼,函數(shù)粒度小更容易被復(fù)用和覆寫贰军。
2 . Inline Method(內(nèi)聯(lián)函數(shù))
在函數(shù)調(diào)用點插入函數(shù)本體,然后移除該函數(shù)蟹肘。
函數(shù)的本體與名稱同樣清楚易懂词疼,間接層太多反而不易理解。
3 . Inline Temp(內(nèi)聯(lián)臨時變量)
將所有對該變量的引用動作帘腹,替換為對它賦值的那個表達式自身贰盗。
4 . Replace Temp with Query(以查詢?nèi)〈R時變量)
將一個表達式提煉到一個獨立函數(shù)中,并將臨時變量的引用點替換為對函數(shù)的調(diào)用阳欲。
臨時變量擴展為查詢函數(shù)舵盈,就可以將使用范圍擴展到整個類。
減少臨時變量球化,使函數(shù)更短更易維護秽晚。
5 . Introduce Explaining Variable(引入解釋性變量)
將該復(fù)雜表達式的結(jié)果放進一個臨時變量,以變量名來解釋其用途筒愚。
6 . Split Temporary Variable(分解臨時變量)
針對每次賦值爆惧,創(chuàng)造一個獨立锨能、對應(yīng)的臨時變量芍耘。
臨時變量會被多次賦值熄阻,容易產(chǎn)生理解歧義斋竞。
如果變量被多次賦值(除了“循環(huán)變量”和“結(jié)果收集變量”)坝初,說明承擔(dān)了多個職責(zé),應(yīng)該分解钾军。
7 . Remove Assignments to Parameters(移除對參數(shù)的賦值)
以一個臨時變量取代該參數(shù)的位置鳄袍。
對參數(shù)賦值容易降低代碼的清晰度拗小;
容易混淆按值傳遞和按引用傳遞的方式 樱哼;
8 . Replace Method with Method object 函數(shù)對象取代函數(shù)
一個大型函數(shù)如果包含了很多臨時變量搅幅,用Extract Method很難拆解,
可以把函數(shù)放到一個新創(chuàng)建的類中息裸,把臨時變量變成類的實體變量界牡,再用Extract Method拆解漾抬。
9 . Substitute Algorithm 替換算法
復(fù)雜的算法會增加維護的成本纳令,替換成較簡單的算法實現(xiàn)平绩,往往能明顯提高代碼的可讀性和可維護性。
七捏雌、在對象之間搬移特性
在面向?qū)ο蟮脑O(shè)計過程中,“決定把責(zé)任放在哪兒”是最重要的事之一纬傲。
最常見的煩惱是:你不可能一開始就保證把事情做對。
在這種情況下算墨,就可以大膽使用重構(gòu)净嘀,改變自己原先的設(shè)計挖藏。
1 . Move Method 移動函數(shù)
類的行為做到單一職責(zé)膜眠,不要越俎代庖柴底。
如果一個類有太多行為粱胜,或一個類與另一個類有太多合作而形成高度耦合焙压,就需要搬移函數(shù)涯曲。
觀察調(diào)用它的那一端幻件、它調(diào)用的那一端绰沥,已經(jīng)繼承體系中它的任何一個重定義函數(shù)贺待。
根據(jù)“這個函數(shù)不哪個對象的交流比較多”麸塞,決定其移動路徑哪工。
2 . Move Field(搬移字段)
如果一個類的字段在另一個類中使用更頻繁,就考慮搬移它傻铣。
3 . Extract Class提煉類
一個類應(yīng)該是一個清楚地抽象非洲,處理一些明確的責(zé)仸两踏。
4 . Inline Class 將類內(nèi)聯(lián)化
Inline Class (將類內(nèi)聯(lián)化)正好于Extract Class (提煉類)相反兜喻。如果一個類丌再承擔(dān)足夠責(zé)仸朴皆、丌再有單獨存在的理由遂铡。將這個類的所有特性搬移到另一個類中扒接,然后移除原類钾怔。
5 . Hide Delegate 隱藏委托關(guān)系
在服務(wù)類上建立客戶所需的所有函數(shù)宗侦,用以隱藏委托關(guān)系
6 . Remove Middle Man(移除中間人)
封裝委托對象也是要付出代價的:每當(dāng)客戶要使用受托類的新特性時矾利,就必須在服務(wù)端添加一個委托函數(shù)梦皮。
隨著委托類的特性(功能)越來越多剑肯,服務(wù)類完全變成了“中間人”,此時就應(yīng)該讓客戶直接調(diào)用受托類呀忧。
很難說什么程度的隱藏才是合適的而账,隨著系統(tǒng)不斷變化泞辐,使用Hide Delegate和Remove Middle Man不斷調(diào)整咐吼。
7 . Introduce Foreign Method 引入外加函數(shù)
你需要為提供服務(wù)的類增加一個函數(shù)锯茄,但你無法修改這個類肌幽。
在客戶類中建立一個函數(shù)喂急,并以第一參數(shù)形式傳入一個服務(wù)類實例煮岁。
客戶類使用Date類的接口画机,但Date類沒有提供nextDay()的接口新症,也不能改Date的源碼:
Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate()+1);
應(yīng)改成
Date newStart = nextDay(previousEnd);
Date nextDay(Date date) {
return new Date(date.getYear(), date.getMonth(), date.getDate()+1);
}
8 . Introduce Local Extension 引入本地擴展
你需要為服務(wù)類提供一些額外函數(shù)徒爹,但你無法修改這個類隆嗅。
建立一個新類胖喳,使它包含這些額外函數(shù)。讓這個擴展品成為源類的子類戒包裝類咕别。
八惰拱、重新組織數(shù)據(jù)
1 . Self Encapsulate Field自封裝字段
為這個字段建立getter/setter函數(shù)偿短,并且只以這些函數(shù)訪問字段翔冀。
- 直接訪問一個字段纤子,導(dǎo)致出現(xiàn)強耦合關(guān)系控硼;
- 直接訪問的好處是易閱讀卡乾,間接訪問的好處是好管理幔妨,子類好覆寫误堡。
2 . Replace Data Value with Object 對象取代數(shù)據(jù)值
一個數(shù)據(jù)項锁施,需要與其他數(shù)據(jù)和行為一起使用才有意義悉抵。將數(shù)據(jù)項改成對象姥饰。
隨著設(shè)計深入列粪,數(shù)據(jù)之間的關(guān)系逐漸顯現(xiàn)出來篱竭,就需要將相關(guān)數(shù)據(jù)及其操作封裝成對象掺逼。
3 . Change value to Reference 將值對象改為引用對象
如果希望修改某個值對象的數(shù)據(jù)吕喘,并且影響到所有引用此對象的地方氯质。
將這個值對象變成引用對象闻察。
組合改為關(guān)聯(lián)
4 . Change Reference to Value 將引用對象改為值對象
引用對象很小且不可變辕漂,將它改成一個值對象钉嘹。
5 . Replace Array with Object 以對象取代數(shù)組
數(shù)組中的元素各自代表不同的東西跋涣。
以對象替換數(shù)組陈辱,對于數(shù)組中的每個元素性置,以一個字段來表示鹏浅。
7 . Change Unidirection Association to Bidirectional 將單向關(guān)聯(lián)改為雙向關(guān)聯(lián)
兩個類都需要使用對方特性隐砸,但其間只有一條單向連接季希。
添加一個反向指針式塌,并使修改函數(shù)能夠同時更新2條連接峰尝。
8 . Change Bidirectional Association to Unidirection將雙向關(guān)聯(lián)改為單向關(guān)聯(lián)
兩個類之間有雙向關(guān)聯(lián)武学,但其中一個類如今不再需要另一個類的特性火窒。去除不必要的關(guān)聯(lián)熏矿。
9 . Replace Magic Number with Symbolic Constant字面常量取代魔法數(shù)
你有一個字面數(shù)值疾捍,帶有特別含義乱豆。
創(chuàng)建一個常量宛裕,根據(jù)其意義為它命名揩尸,并將上述的字面數(shù)值替換為這個常量岩榆。
10 . Encapsulate Field 封裝字段
你的類中存在一個public字段勇边。將它聲明為private粒褒,并提供相應(yīng)的訪問函數(shù)奕坟。
11 . Encapsulate Coolection 封裝集合
如果一個函數(shù)返回一個集合月杉,應(yīng)改為返回該集合的一個只讀副本苛萎,并在這個類中提供添加/移除集合元素的函數(shù)首懈。
12 . Replace Record with Data Class 以數(shù)據(jù)類取代記錄
13 . Replace Type Code with Class 以類來取代類型碼
類中有一個數(shù)值類型碼究履,但它并不影響類的行為最仑。以一個新的類替換該數(shù)值類型碼泥彤。
14 . Replace Type Code with Subclasses 以子類來取代類型碼
如果類型碼不會影響宿主類的行為吟吝,可以使用Replace Type Code with Class (以類取代類型碼)來處理剑逃。
但如果類型碼會影響宿主類的行為蛹磺,最好的辦法是多態(tài)來處理變化行為萤捆。
15 . Replace Type Code with State/Strategy 以狀態(tài)/策略取代類型碼
你有一個類型碼市怎,它會影響類的行為焰轻,但你無法提供繼承手法消除它辱志。以狀態(tài)對象替代類型碼揩懒。
16 . Replace Subclass with Fieldls 以字段取代子類
你的各個子類的唯一差別只在“返回常量數(shù)據(jù)”的函數(shù)身上已球。
修改這些函數(shù),使它們返回超類中的某個字段(新增)阔蛉,然后銷毀子類状原。
九颠区、簡化表達式
在面向?qū)ο蟮脑O(shè)計過程中毕莱,表達式的使用通常較少,
因為很多條件行為都被多態(tài)機制處理掉了质和,所以條件的擴展更為容易饲宿。
有時候條件邏輯可能十分復(fù)雜瘫想,可以使用本章提供的一些重構(gòu)手法來簡化它們国夜。
1 . Decompose Conditional 分解條件表達式
程序中筹裕,復(fù)雜的條件邏輯是最常導(dǎo)致復(fù)雜度上升的地點之一朝卒。
可以將它分解為多個獨立函數(shù)抗斤,根據(jù)每個小塊代碼的用途丈咐,為分解的新函數(shù)命名瑞眼,從而更清楚的表達意圖。
if (date.before(SUMMER_START) || date.after(SUMMER_END)){
charge = quantity * winterRate;
} else{
charge = quantity * summerRate;
}
改為
if (notSummer(date)){
charge = winterCharge(quantity);
} else{
charge = summerCharge(quantity);
}
boolean notSummer(Date date) {
return date.before(SUMMER_START) || date.after(SUMMER_END);
}
double winterCharge(double quantity) {
return quantity * winterRate;
}
2 . Consolidate Conditional Expression 合并條件表達式
一系列條件測試棵逊,都得到相同結(jié)果伤疙。
將這些測試合并為一個條件表達式,并將這個條件表達式提煉為一個獨立函數(shù)歹河。
3 . Consolodate Duplicate Conditional Fragments 合并重復(fù)的條件片段
在條件表達式的每個分支上有著相同的一段代碼。將這段重復(fù)代碼移到條件表達式之外秸歧。
4 . Remove Control Flag 移除控制標(biāo)記
以break或return語句取代控制標(biāo)記厨姚。
5 . Replace Nested Conditional with Guard Clauses 以衛(wèi)語句取代嵌套條件表達式
double getPayAmount (){
double result;
if (_isDead)
result = deadAmount();
else{
if (_isSep)
result = SepAmount();
else{
if (isRetired)
result = retireAmount();
else
result = normalAmount();
}
}
return result;
}
改為
double getPayAmount () {
if (_isDead)
return deadAmount();
if (_isSep)
return SepAmount();
if (isRetired)
return retireAmount();
return normalAmount();
}
6 . Replace Conditional with Polymorphism 以多態(tài)取代條件表達式
一個條件表達式,它根據(jù)對象類型的丌同而選擇丌同的行為键菱。
將這個條件表達式的每個分支放進一個子類的覆寫函數(shù)中谬墙,然后將原始函數(shù)聲明為抽象函數(shù)。
8 . Introduce Assertion 引入斷言
某一段代碼需要對程序狀態(tài)做出某種假設(shè)经备。以斷言明確表現(xiàn)這種假設(shè)拭抬。
- 使用斷言明確標(biāo)明對輸入條件的嚴(yán)格要求和限制;
- 斷言可以輔助交流和調(diào)試侵蒙。
double getExpenseLimit () {
//should have either expense limit or a primary project
return (_expLimit != NULL_EXPENSE) ? _expLimit : _primaryPro.getExpenseLimit();
}
改為
double getExpenseLimit () {
Assert.isTrue((_expLimit != NULL_EXPENSE) || _primaryPro != NULL );
return (_expLimit != NULL_EXPENSE) ? _expLimit : _primaryPro.getExpenseLimit();
}
十造虎、簡化函數(shù)調(diào)用
在面向?qū)ο蟮脑O(shè)計技術(shù)中,最重要的概念莫過于“接口”(interface)纷闺。
容易被理解和被使用的接口均芽,是開發(fā)良好面向?qū)ο筌浖年P(guān)鍵昔期。
1 . Rename Method 函數(shù)改名
2 . Add Parameter 添加參數(shù)
某個函數(shù)需要從調(diào)用端得到更多信息六剥。為此函數(shù)添加一個對象參數(shù)尸执,讓該對象帶進函數(shù)所需信息。
3 . Remove Parameter(移除參數(shù))
函數(shù)不再需要某個參數(shù)時浸卦,將其移除
4 . Separate Query from Modifier 將查詢函數(shù)和修改函數(shù)分離
5 . Parameterize Method 令函數(shù)攜帶參數(shù)
若干函數(shù)做了類似的工作署鸡,但在函數(shù)本體中卻包含了不同的值。
建立一個單一函數(shù),以參數(shù)表達那些不同的值靴庆。
6 . Replace Parameter with Explicit Methods 以明確函數(shù)取代參數(shù)
某個函數(shù)完全取決于參數(shù)值而采取不同行為时捌,為了獲得一個清晰的接口,
針對該參數(shù)的每一個可能值撒穷,建立一個獨立函數(shù)匣椰。
void setValue (String name, int value) {
if (name.euqals("height") {
_height = value;
return ;
}
if (name.euqals("width") {
_width = value;
return ;
}
Assert.shouldNeverReachHere();
}
改為
void setHeight ( int arg) {
_height = arg;
}
void setWidth ( int arg) {
_width = arg;
}
7 . Preserve whole object 保持對象完整
如果從對象中取出若干值裆熙,將它們作為某一次函數(shù)調(diào)用時的參數(shù)端礼。改為傳遞整個對象。
除了可以使參數(shù)列更穩(wěn)固外入录,還能簡化參數(shù)列表蛤奥,提高代碼的可讀性。
此外僚稿,使用完整對象凡桥,被調(diào)用函數(shù)可以利用完整對象中的函數(shù)來計算某些中間值。
- 不過事情總有2面:如果你傳的是數(shù)值蚀同,被調(diào)用函數(shù)就叧依賴于這些數(shù)值缅刽。但如果你傳遞的是整個對象,被調(diào)用函數(shù)所在的對象就需要依賴參數(shù)對象蠢络。如果這會使你的依賴結(jié)構(gòu)惡化衰猛,那么就不該使用。
- 有的觀點認為:如果被調(diào)用函數(shù)只需要參數(shù)對象的其中一項數(shù)值刹孔,那么只傳遞那個數(shù)值會更好啡省。這個觀點不能被認同:因為傳遞一項數(shù)值和傳遞一個對象,至少在代碼清晰度上是一致的髓霞。更重要的考量應(yīng)該放在對象之間的依賴關(guān)系上卦睹。
8 . Replace Parameter with Methods 以函數(shù)取代參數(shù)
對象調(diào)用某個函數(shù),并將所得結(jié)果作為參數(shù)方库,傳遞給另一個函數(shù)结序。
而接受該參數(shù)的函數(shù)本身也能夠調(diào)用前一個函數(shù)。
讓參數(shù)接受者去除該項參數(shù)纵潦,并直接調(diào)用前一個函數(shù)徐鹤。
9 . Introduce Parameter Object 引入?yún)?shù)對象
如果一組參數(shù)總是一起被傳遞,以一個對象取代這些參數(shù)酪穿。
14 . Replace Error Code with Exception 以異常取代錯誤碼
15 . Replace Exception with Test 以測試取代異常
面對調(diào)用者可以預(yù)先檢查的條件凳干,在調(diào)用函數(shù)之前應(yīng)該先做檢查,而不是直接捕獲異常被济。
十一救赐、處理概況關(guān)系
在面向?qū)ο蟮脑O(shè)計過程中,概括(繼承)關(guān)系是其核心特性。
良好的繼承體系可以顯著地提高程序的易讀性和易理解性经磅,增加了未來修改和擴展的靈活性泌绣。
1 . Pull Up Field 字段上移
兩個子類擁有相同的字段。將該字段移至超類预厌。
2 . Pull up Method 函數(shù)上移
有些函數(shù)阿迈,在各個子類中產(chǎn)生完全相同的結(jié)果。將該函數(shù)移至超類轧叽。
3 . Pull up Constructor Body 構(gòu)造函數(shù)本體上移
各個子類中擁有一些極造函數(shù)苗沧,它們的本體幾乎完全一致。
在超類中新建一個構(gòu)造函數(shù)炭晒,并在子類構(gòu)造函數(shù)中調(diào)用它待逞。
4 . Push down Method 函數(shù)下移
超類中的某個函數(shù)只與部分子類有關(guān)。將這個函數(shù)移到相關(guān)的那些子類去网严。
5 . Push down Fiedld 字段下移
超類中的某個字段只被部分子類用到识樱,將這個字段移到需要它的那些子類去。
6 . Extract Subclass 提煉子類
類中的某些特性只被某些實例用到震束。新建一個子類怜庸,將這部分特性移到子類中。
7 . Extract Superclass 提煉超類
兩個類有相似特性垢村。為這2個類建立一個超類割疾,將相同特性移至超類。
10 . From TemPlate Method 塑造模板函數(shù)
一些子類肝断,其中相應(yīng)的某些函數(shù)以相同的順序執(zhí)行類似的操作杈曲,但各個操作的細節(jié)不同。
將這些操作分別放迚獨立的函數(shù)中胸懈,并保持它們都有相同的簽名担扑,于是原函數(shù)也就變得相同了,然后將原函數(shù)上移至超類趣钱。
11 . Replace Inheritance with delegation 以委托取代繼承
某個子類只使用超類接口中的一部分涌献,或是根本不需要繼承而來的數(shù)據(jù)。
在子類中新建一個字段用以保存超類首有;調(diào)整子類函數(shù)燕垃,令它改而委托超類;然后去掉2者之間的繼承關(guān)系井联。
12 . Replace delegation with Inheritance 以繼承代替委托
兩個類之間使用委托關(guān)系卜壕,并經(jīng)常為整個接口編寫許多簡單的委托函數(shù)。讓委托類繼承受托類烙常。
十二轴捎、大型重構(gòu)
在一知半解的情況下做出的設(shè)計決策,一旦堆積起來,也會使你的程序陷于癱瘓侦副。通過重構(gòu)侦锯,可以保證隨時在程序中反映出完整的設(shè)計思路。
1 . 1. Tease apart Inheritance 梳理并分解繼承體系
某個繼承體系同時承擔(dān)兩項責(zé)任秦驯。
建立兩個繼承體系尺碰,并通過委托關(guān)系讓其中一個可以調(diào)用另一個。
2 . Convert Procedural design to Objects 將過程化設(shè)計轉(zhuǎn)化為對象設(shè)計
你手上有一些傳統(tǒng)過程風(fēng)格的代碼 译隘。
將數(shù)據(jù)記錄變成對象亲桥,將大塊的行為分成小塊,并將行為移入相關(guān)對象之中细燎。
3 . Separate Domain from from Presention 將領(lǐng)域和表述/顯示分離
某些GUI類之中包含了領(lǐng)域邏輯 ,將領(lǐng)域邏輯分離出來两曼,為它們建立獨立的領(lǐng)域類
4 . Extract Hierarchy 提煉繼承體系
你有某個類做了太多工作皂甘,其中一部分工作是以大量條件表達式完成的 ,
建立繼承體系玻驻,以一個子類表示一種特殊情況
十三、重構(gòu)偿枕、復(fù)用與現(xiàn)實
1 . 項目開始時的選擇
- 重寫整個程序
可以依賴自己的經(jīng)驗糾正程序中的錯誤璧瞬,但誰來付錢呢?你又如何保證新的系統(tǒng)能夠完成舊系統(tǒng)所做的每一件事呢渐夸? - 復(fù)制嗤锉、修改、擴展
隨著時間流逝墓塌,錯誤會不斷地被復(fù)制瘟忱、被傳播,程序變得臃腫苫幢,當(dāng)初的設(shè)計開始腐敗變質(zhì)访诱,修改的整體成本逐漸上升。 -
重構(gòu)是兩個極端之間的中庸之道
通過重新組織軟件結(jié)構(gòu)韩肝,重構(gòu)使得設(shè)計思路更詳盡明確触菜,抽取可復(fù)用組件,使得軟件架構(gòu)更清晰哀峻,增加新功能更容易涡相,使程序更簡潔有力。
2 . 為什么開發(fā)者不愿意重構(gòu)他們的程序剩蟀?
- 不知道如何重構(gòu)催蝗?
- 重構(gòu)的收益是長遠的,也許那時你已經(jīng)離開當(dāng)初的職位了育特,所以沒有動力去實施重構(gòu)丙号。
- 代碼重構(gòu)是一項額外工作,老板并不會為此付錢。
- 重構(gòu)有可能破壞現(xiàn)有程序槽袄。
3 . 重構(gòu)以獲得短期收益
重構(gòu)可以帶來短期利益烙无,讓軟件更易修改、更易維護遍尺。重構(gòu)只是一種手段截酷,不是目的。
它是“程序員或程序開發(fā)團隊如何開發(fā)并維護自己的軟件”這一更寬廣場景的一部分乾戏。
4 . 重構(gòu)為設(shè)計模式
-
設(shè)計模式
設(shè)計模式(Design pattern)是一套被反復(fù)使用迂苛、多數(shù)人知曉的、經(jīng)過分類編目的鼓择、代碼設(shè)計經(jīng)驗的總結(jié)三幻。
使用設(shè)計模式是為了可重用代碼、讓代碼更容易被他人理解呐能、保證代碼可靠性念搬。
每種模式在現(xiàn)在中都有相應(yīng)的原理來與之對應(yīng),并且描述了一個在我們周圍不斷重復(fù)發(fā)生的問題摆出,以及該問題的核心解決方案朗徊。 -
重構(gòu)為設(shè)計模式
在很多時候,將代碼重構(gòu)為符合設(shè)計模式的要求也是重構(gòu)的一個最佳目標(biāo)偎漫。
總結(jié)
本書主要涉及重構(gòu)中的各種細節(jié)問題爷恳,
從如何識別代碼的壞味道,
到重新組織函數(shù)象踊、對象温亲、數(shù)據(jù),
再到簡化表達式杯矩、簡化函數(shù)調(diào)用栈虚,
再到更高層級的處理概況(繼承)關(guān)系、大型重構(gòu)菊碟。
熟練掌握重構(gòu)技巧节芥,需要在學(xué)習(xí)工作中反復(fù)的練習(xí),并不斷思考為什么要這么做(違反面向?qū)ο笤瓌t逆害、壞味道等等)头镊。