重構改善即有程式碼的設計
(一)重構原則
1、何謂重構
對軟體內部結構的一種調整,目的是在不改變軟體可觀察行為的前提下,提高其可理解性,降低其修改成本。
另一種解釋是:使用一系列重構手法,在不改變軟體可觀察行為的前提下,調整其結構。
重構不止是程式碼整理,它提供了一種高效且受控的程式碼整理技術
2、為何重構
良好的設計是快速開發的根本
3、何時重構
任何情況下我都反對專門撥出時間進行重構。重構本來就不是一件應該特別撥出時間做的事情,重構應該隨時隨地的進行。
三次法則
第一次做某件事情是隻管去做;第二次做類似的事情會產生反感;第三次再做類似的事,你就應該重構
- 最常見的重構時機是想給軟體新增新特性的時候;
重構的另個一原動力是:程式碼的設計無法幫助我輕鬆的新增所需要的特性
- 修改錯誤的時候
- review程式碼的時重構
間接層和重構
電腦科學是這樣一門科學:它相信所有的問題都可以通過增加一個間接層來解決。
大多數重構都為程式引入了更多的間接層,重構往往把大型的物件拆成多個小型的物件,把大型的函式拆成多個小型的函式。
但是,間接層是一把雙刃劍。每次把一個東西分成兩份,你就需要多管理一個東西。如果某個物件委託另一個物件,後者又委託另一個物件,程式會愈加難以閱讀。
何時不該重構:有時候既有程式碼實在太混亂,重構它還不如重新寫一個來得簡單。
重寫而非重構的一個清楚訊號是:現有程式碼根本不能正常運作。
(二)程式碼的壞味道
1、重複程式碼
如果你在一個以上的地點看到相同的程式結構,那麼可以肯定:設法將它們合二為一,程式會變得更好 。
- 同一個類中有相同的表示式:提煉出重複的程式碼,然後讓兩個地方都呼叫被提煉出來的那一段程式碼;
- 兩個互為兄弟的子類內含有相同的表示式:提煉出相同程式碼,將它推入超類內;
- 兩個毫不相干的類中出現:將重複的程式碼提煉到一個獨立的類中。
2、過長的類
擁有短函式的物件活得比較好、比較長。
間接層所能帶來的全部利益——解釋能力、共享能力、選擇能力——都是由小型函式支援的。
每當感覺需要以註釋來說明點什麼的時候,我們就把需要說明的東西寫進一個獨立的函式中。
如何確定提煉哪一段程式碼?尋找註釋是一個很好的技巧。它們通常能指出程式碼用途和實現手法之間的語義距離。如果程式碼前方有一行註釋,就是提醒你:可以將這段程式碼替換成一個函式。
條件表示式和迴圈常常也是提煉的訊號。
3、過大的類
如果想利用單個類做太多的事情,其內往往就會出現太多實力變數。
類內如果有太多程式碼,也是程式碼重複、混亂病最終走向死亡的源頭。
4、過長引數列
太長的引數列難以理解,太多的引數會造成前後不一致、不容易使用,而且一旦你需要更多資料,就不得不修改它。如果將物件傳遞給函式,大多數修改都將沒有必要。
5、發散式變化
如果某個類經常因為不同的原因在不同的方向上發生變化,那麼此時也許將這個物件分成兩個會更好,這麼一來每個物件就可以只因為一種變化而需要修改。
6、散彈式修改
如果沒遇到某種變化,你都必須在許多不同的類內做出許多小修改,你所面臨的壞味道就是散彈式修改。如果需要修改的程式碼散佈四處,你不但很難找到它們,也很容易忘記某個重要的修改。
把所有需要修改的程式碼放進同一個類中,如果眼下沒有合適的類可以安置這些程式碼就創造一個。
7、依戀情結
物件技術的要點在於:將資料和對資料的操作行為包裝在一起
有一種經典的氣味是:函式對某個類的興趣高過對自己所處類的興趣。
某個函式為了計算某個值,從另一個物件那呼叫幾乎半打的取值函式。
一個函式往往會用到幾個類的功能,那麼它該置於何處?我們的原則是:判斷哪個類擁有最大被此函式使用的資料,然後就把這個函式和那些資料放在一起。
8、資料泥團
很多地方看到相同的三四項資料一起出現。這些總是綁在一起出現的資料應該擁有屬於他們自己的物件。
首先找到這些資料以欄位形式出現的地方,將它們提煉到一個獨立的物件中。這麼做的直接好處是可以將很多引數列縮短簡化函式呼叫。
9、基本型別偏執
物件的一個極大價值在於:它們模糊了橫旦與基本資料和體積較大的類之間的界限
物件技術的新手通常不願意在小任務上運用小物件——結合數值和比重的money類、有一個起始值和一個結束值組成的range類。將原本單獨存在的數值替換成物件,從而走出傳統的洞窟,進入炙手可熱的物件世界。
10、switch驚悚現身
面向物件的一個最明顯的特徵是:少用switch語句
一看到switch語句,就應該考慮以多型來替換它。
如果只是在單一函式中有些選擇例項,且並不想改動它們,那麼多型就有點殺雞用牛刀了。
11、平行整合體系
每當你為某個類增加一個自雷,必須也為另一個類相應增加一個子類。
消除這種重複性的一般策略是:讓一個繼承體系的例項引用另一個繼承體系的例項。
12、冗餘類
某個類原本對得起自己的身價,但重構使它身形縮水,不再做那麼多工作,這個時候請讓這個類莊嚴赴義吧。
13、誇誇其談未來性
企圖以各種各樣的鉤子和特殊情況來處理一些非必要的事情,這種懷味道就出現了。如果用到了那就值得去做,如果用不到那就不值得,只會擋你的路,所以把它挪開吧。
如果你的某個抽象類其實沒有起到太大的作用,函式上的某些引數未被使用...可以移除它們了。
14、令人迷惑的暫時欄位
某個例項變數僅為某種特定的情況而設。這樣的程式碼讓人不易理解。在變數未被使用的情況下猜測當初其設定目的,會讓你發瘋的。
15、過度耦合訊息鏈
如果你看到使用者向一個物件請求另一個物件,然後再向後者請求另一個物件,然後再請求另個一物件........這就是訊息鏈。採用這種方式,意味著客戶程式碼將與查詢過程中的導航結構緊密耦合。一旦物件間的關係發生任何變化,客戶端就不得不做出相應的修改。
16、中間人
封裝往往伴隨著委託。你也許會看到某個類介面有一半的函式都委託給其他類,這樣就是過度運用。
17、狎暱關係
有時會看到兩個類過於親密,話費太多的時間去探究彼此的private成分。過分狎暱的類必須拆散,幫它們劃清界線,從而減少狎暱行徑。
繼承往往造成過度親密,因為子類對超類的瞭解總是超過後者的主觀願望。如果你覺得該讓孩子獨立生活了,讓他離開繼承。
18、異曲同工的類
兩個函式做同一件事,卻有著不同的簽名。
19、不完美的類庫
類庫函式構造的不夠好,又不能修改它們:
-
如果只想修改類的一兩個函式,可以引入外加函式。
-
如果想要新增一大堆額外行為,建立一個新類包含這些額外行為,讓其成為子類。
20、純稚的資料類
純稚的資料類是指:它們擁有一些欄位,以及用於訪問(讀寫)這些欄位的函式,除此之外一無長物。
封裝public欄位;
恰當封裝容器類欄位;
移除不應修改的欄位的設定函式;
提煉呼叫函式以隱藏取值/設值函式;
21、被拒絕的遺贈
子類只運用了父類的一部分函式和資料。 為子類建立一個兄弟類,將所有用不到的欄位/函式下移至兄弟類,保證超類的純粹;
22、過多的註釋
註釋之所以存在是因為程式碼很糟糕 。註釋的最高境界——程式碼即註釋。
當你感覺需要撰寫註釋時,請先嚐試重構,試著讓所有的註釋都變得多餘。
(三)重新組織函式
1、提煉函式
動機:看到一個過長的函式或者一段需要註釋才能讓人理解用途的程式碼,將這段程式碼放一個獨立的函式中;
做法:
-
創造一個新函式,根據這個函式的意圖來命名它;
只要新函式的名稱能夠以更好的方式昭示程式碼意圖,你也應該提煉它。但如果想不到一個更有意義的名稱就別動
-
將提煉的程式碼從原函式複製到新建的目標函式中;
-
將被提煉程式碼段中需要讀取的區域性變數,當作引數傳遞給目標函式;
-
在源函式中,將被提煉程式碼段替換為目標函式呼叫。
2、行內函數
一個函式的本體與名稱同樣清楚易懂。在函式呼叫點插入函式本體,然後移除該函式。
動機:
- 一群組織不甚合理的函式。你可以將它們都內聯到一個大函式中,再從中提煉出組織合理的小型函式。
- 使用的太多的間接層,使得系統中的所有函式都似乎只是對另一個函式的簡單委託,造成在委託動作之間暈頭轉向。
做法:
-
檢查函式,確定不具備多型;
如果子類繼承了這個函式,就不要將此函式內聯,因為子類無法複寫一個根本不存在的函式。
-
找出這個函式的所有呼叫點;
-
將這個函式的所有呼叫點都替換成函式本體。
3、內聯臨時變數
有一個臨時變數,只被一個簡單的表達是賦值一次,而它妨礙了其他重構手法。將所有對該變數的引用動作,替換為對它賦值的那個表示式自身
double basePrice = anOrder.basePrice(); return (base > 10000 );
替換為:
return (anOrder.basePrice > 1000);
4、以查詢取代臨時變數
你的程式以一個臨時變數儲存某一表達式的運算結果。將這個表示式提煉到一個獨立的函式中。將這個臨時變數的所有引用點替換為對新函式的呼叫。此後,新函式就可被其他函式使用。
double basePrice = quantity * timePrice; if(basePrice > 1000){ return basePrice * 09.5; } else { return basePrice * 0.98; }
替換為:
if(basePrice() > 1000){ return basePrice * 09.5; } else { return basePrice * 0.98; } double basePrice(){ return quantity * timePrice; }
臨時變數只在所屬的函式中可見,如果把臨時變數替換為一個查詢,那麼同一個類中的所有函式都將可以獲得這個份資訊,這將帶給你極大的幫助,使你能夠為這個類編寫更清晰的程式碼。
5、引入註釋性變數
你有一個複雜的表示式。將該複雜表示式(或其中一部分)的結果放進一個臨時變數,以此變數名稱來解釋表示式用途。
if ((platform.toUpperCase().indexOf("MAC") > -1) && (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize >0){ //do smothing }
替換為:
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1; final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1; final boolean wasResized = resize >0; if(isMacOs && isIEBrowser && wasInitialized() && wasResized){ //do smothing }
表示式有可能非常複雜難以理解。這種情況下,臨時變數可以幫助你將表示式分解為比較容易管理的形式。
在條件邏輯中,你可以用這項重構將每個條件子句提煉出來,以一個良好命名的臨時變數來解釋對應條件子句的意義。另一種情況是:在較長的演算法中,可以運用臨時變數來解釋每一步運算的意義。
6、分解臨時變數
你的程式有某個臨時變數被賦值超過一次,它既不是迴圈變數,也不被用於收集計算結果。針對每次賦值,創造一個獨立、對應的臨時變數。
double temp = 2 * (height + width); System.out.println(temp); temp = height * width; System.out.println(temp);
替換為:
double perimeter = 2 * (height + width); System.out.println(perimeter); double area = height * width; System.out.println(area);
如果臨時變數被賦值超過一次,就意味著它們在函式中承擔了一個以上的責任。如果臨時變數承擔多個責任,它就應該被替換為多個臨時變數。每個變數只承擔一個責任,同一個臨時變數承擔兩件不同的事情會令程式碼閱讀者糊塗
7、移除對引數的賦值
程式碼對一個引數進行復制。以一個臨時變數取代該引數的位置。
int discount (int inputVal, int quantity, int yearToData){ if(inputVal > 50) inputVal -= 2; }
替換為:
int discount (int inputVal, int quantity, int yearToData){ int result = inputVal; if(inputVal > 50) result -= 2; }
如果程式碼的語義是按引用傳遞的,請在呼叫段檢查呼叫後是否還使用了這個引數。
8、替換演算法
想要把某個演算法替換為另一個更清晰的演算法。將函式本體替換成為另一個演算法。
String foundPerson(String[] people){ for(int i = 0;i < people.length; i++){ if(people[i].equals("Don")){ return "Don"; } if(people[i].equals("John")){ return "John"; } if(people[i].equals("Kent")){ return "Kent"; } } return ""; }
替換為:
String foundPerson(String[] people){ List candidates = Arrays.asList(new String[]{"Don", "John", "Kent"}); for(int i = 0;i < people.length; i++){ if(candidates.contains(people[i])){ return prople[i]; } } return ""; }
(四)在物件之間搬移特性
在物件設計過程中,決定把責任放在哪兒
是即使不是最重要的事,也是最重要的事之一。
常常只使用搬移函式
和搬移欄位
簡單地移動物件行為,就可以解決這些問題。如果這兩個重構手法都需要用到,我會首先使用搬移欄位
,再使用搬移方法
。
如果一個類承擔了太多責任而變得臃腫不堪,這種情況下會使用提煉類
將一部分責任分離出去。如果一個類變得太不負責任,使用將類內聯化
將它融入到另一個類中。
1、搬移函式
你的程式中,有個函式與其所駐類之外的另個一類進行跟過的交流:呼叫後者或被後者呼叫。在該函式最長引用的類中建立一個有著類似行為的新函式。將舊函式變成一個單純的委託函式,或者將舊函式完全移除。
如果一個類有太多行為,或如果一個類與另一個類有太多合作而高度耦合,就需要搬移函式。可以是系統中的類更簡單
2、搬移欄位
程式中,某個欄位被其所駐類之外的另一個類更多的用到。在目標類新建一個欄位,修改原欄位的所有使用者,令他們改用新欄位
3、提煉類
某個類做了應該由兩個類做的事。建立一個新類,將相關欄位和函式從就類搬到新類。
4、將類內聯化
某個類沒有做太多的事情,不在承擔足夠責任,不再有的那單獨存在的理由。將這個類的所有特性搬移到另一個類中,然後移除原類。
5、隱藏“委託關係”
在服務類上建立客戶所需要的所有函式,用來隱藏委託關係。
封裝意味每個物件都應該少了解系統的其他部分。一旦發生變化,需要了解這一變化的物件就會比較少。
如果某個客戶先通過服務物件的欄位得到另一個物件,然後呼叫後者的函式。那麼客戶就必須知曉這一層委託關係。萬一委託關係變化,客戶也要相應變化。
6、移除中間人
某個類做了過多的簡單委託。讓客戶直接呼叫委託類。
每當客戶要使用手委託類的新特性時,你就必須在服務端新增一個簡單委託函式。隨著受委託類的特性越來越多,這一過程會讓你很痛苦。
7、引入外加函式
你需要為提供服務的類增加一個函式,但你無法修改這個類。在客戶類中建立一個函式,並以第一引數形式傳入一個服務類例項。
Date newStart = new Date(year, month, date + 1);
替換為:
Date newStart = nextDay(nowDate); private static Date nextDay(Date arg){ retrun new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1); }
如果可以修改原始碼,你可以自行新增一個新函式;如果不能,你就得在客戶端編碼,補足你要的那個函式
8、引入本地擴充套件
你需要為服務類踢狗一些額外函式,但你無法修改這個類。建立一個新類,使它包含這些額外函式。讓這個擴充套件品成為源類的子類或包裝類。
(五)重新組織資料
1、自封裝欄位
直接訪問一個欄位。為這個欄位建立取值/設值函式,並且只以這些函式來訪問欄位。
private int low, high; boolean includes(int arg){ retrun arg >= low && arg <= high; }
替換為:
private int low, high; boolean includes(int arg){ retrun arg >= getLow() && arg <= getHigh(); } int getLow(){ retrun low; } int getHigh(){ return high; }
在“欄位訪問方式”這個問題上,存在兩種截然不同的觀點:
- 在該變數定義所在的類中,你可以自由的訪問。
-
即使在這個類中你也應該只使用訪問函式間接訪問。
間接訪問的好處是:子類可以通過複寫一個函式而改變獲取資料的途徑;它支援更靈活的資料管理方式,例如延遲初始化。
2、以物件取代資料值
你有一個數據項,需要與其他資料和行為一起使用才有意義。將資料項變為物件。
一開始你肯能會用一個字串來表示“電話號碼”概念,但是隨後你會發現,電話號碼需要“格式化”、“區號”之類的行為。這時候就需要為帶替換的數值新建一個類。
3、將值物件改為引用物件
你從一個類衍生出許多彼此相等的例項,希望將它們替換為同一個物件。將這個值物件變成引用物件。
4、將引用物件改為值物件
你有一個引用物件,很小且不可改變,而且不易管理。將它變成一個值物件。
5、以物件取代陣列
你有一個數組,其中的元素各自代表不同的東西。以物件替換陣列。對於陣列中的每個元素,以一個欄位來表示
6、複製“被監視資料”
你有一些領域資料置身GUI控制元件中,而領域函式需要訪問這些資料。將該資料複製到一個領域物件中。建立一個Observer模式,用以同步領域物件和GUI物件內的重複資料。
7、將單向關聯改為雙向關聯
兩個類都需要使用對方特性,但其間只有一條單向連線。新增一個反向指標,並使修改函式能夠同時更新兩條連線。
8、將雙向關聯改為單向關聯
兩個類之間有雙向關聯,但其中一個類如今不再需要另一個類的特性。去除不必要的關聯。
9、以字面常量取代魔數
你有一個字面數值,帶有特別含義。創造一個常量,根據其意義為它命名,並將上述的字面數值替換為常量。
10、封裝欄位
你的類中存在一個public欄位。將它宣告為private,並提供相應的訪問函式。
11、封裝集合
有個函式返回一個集合。讓這個函式返回該集合的一個只讀副本,並在這個類中提供新增/移除集合元素的函式。
(六)簡化條件表示式
1、分解條件表示式
有一複雜的條件語句。從if、then、else三個段落中分別提煉出獨立函式。
2、合併表示式
你有一系列條件測試,都得到相同結果。將這些測試合併為一個條件表示式,並將這個條件表示式提煉成一個獨立函式。
3、合併重複的條件程式碼
在表示式的每個分支上都執行了相同的一段程式碼。將這段重複程式碼搬移到條件表示式之外。
4、移除控制標記
在一系列布林表示式中,某個變數帶有”控制標記”的作用。以break/return語句取代控制標記。
5、以多型取代條件表示式
有個條件表示式根據物件型別的不同而選擇不同的行為。將這個條件表示式的每個分支放進一個子類內的覆寫函式中,然後將原始函式宣告為抽象函式
(七)簡化函式呼叫
1、函式改名
函式的名稱未能揭示其用途。修改函式名稱。
2、新增引數
某個函式需要從呼叫端得到更多資訊。為此函式新增一個物件引數,讓該物件帶僅函式所需資訊。
3、移除引數
函式本體不再需要某個引數。去除引數。
4、分離查詢函式和修改函式
某個函式既返回物件狀態值,又修改物件值。建立兩個不同函式,其中一個負責查詢,另一個負責修改。
5、令函式攜帶引數
若干函式做了類似的工作,但在函式本體中包含了不同的值。建立單一函式,以引數表達那些不同的值。
有這樣兩個函式:它們做著類似的工作,但因少數幾個值致使行為略有不同。在這種情況下,你可以將這些各自分離的函式同一起來,並通過引數來處理那些變化情況,用以簡化問題。
6、以明確函式取代引數
你有一個函式,其中完全取決於引數值而採用不同行為。針對該引數的每一個可能值,建立一個獨立函式。
如果某個引數有多種可能的值,而函式內又以條件表示式檢查這些引數值,並根據不同引數值做出不同的行為,那麼就應該使用本項重構。
7、保持物件完整
從某個物件中取出若干值,將它們作為某一次函式呼叫時的引數。改為傳遞整個物件。
8、以函式取代引數
物件呼叫某個函式,並將所得結果作為引數,傳遞給另一函式,而接受該引數的函式本身也能夠呼叫前一個函式。讓引數接受者去除該引數,直接呼叫前一個函式。
9、引入引數物件
某些引數總是很自然的同時出現。以一個物件取代這些引數。
10、移除設值函式
類中某個欄位在物件建立時被設值,然後不再改變。去掉該欄位的所有設值函式。
11、隱藏函式
某個函式,從來沒有被其他任何類用到 。將函式修改為private。
12 、以工廠函式取代建構函式
希望在建立物件時不僅僅是做簡單的建構動作 。將建構函式替換為工廠函式。
(八)處理概括關係
1、欄位上移
兩個子類擁有相同的欄位。將該欄位移至超類。
2 、函式上移
有些函式在各子類中產生完全相同的結果。將該函式移至超類。
3 、建構函式本體上移
各個子類中有一些建構函式本體幾乎完全一致 。在超類中新建一個建構函式,並在子類建構函式中呼叫它。
4、函式下移
超類中的某個函式只與部分(而非全部)子類用到。將函式移到相關的子類中。
5、欄位下移
超類中的某個欄位只被部分(而非全部)子類用到。將欄位移到需要它的子類中。
6、提煉子類
類中的某些特性只被某些(而非全部)例項用到。新建一個子類,將上述部分的特性移到子類中。
7、提煉超類
兩個類有相似特性。為這兩個類建立一個超類,將相同特性移至超類。
8、提煉介面
若干客戶使用類介面中的同一子集,或兩個類的介面有部分相同。將相同的子集提煉到一個獨立介面中。
9、摺疊繼承體系
超類和子類之間無太大區別。將它們合為一體。
10、塑造模板函式
子類中某些函式以相同順序執行類似操作,但各操作細節略有不同。將操作放進獨立函式(保持簽名相同),然後將它們移至超類。
11、以委託取代繼承
某個子類只使用超類介面中的一部分或根本不需要繼承而來的資料。子類新建欄位儲存超類,調整子類函式為委託超類,取消繼承關係。
12、以繼承取代委託
你在兩個類中使用委託關係,並經常為整個介面編寫許多極簡單的委託函式。讓委託類繼承受託類。