重构—改善既有代码的设计
概述
1.1 參考資料
1.2 何謂重構
首先要說明的是:視上下文不同,重構的定義可以分為名詞和動詞兩種形式。
1. 重構(名詞):對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。
2. 重構(動詞):使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整其結構。
根據重構的定義可知,重構其實就是優化代碼的結構,使其閱讀性更好。需要強調的是:重構不能改變軟件可觀察的行為——重構之后軟件功能一如既往。任何用戶,不論最終用戶或其它程序員,都不知道已經有東西發生了變化。
1.3 為何重構
重構可以幫助你始終良好的控制自己的代碼。重構就是一個工具,它可以用于以下幾個目的。
1.3.1 重構改進軟件設計
當人們只為短期目的,或是在完全理解整體設計之前,就貿然修改代碼,程序將逐漸失去自己的結構,就很難通過閱讀源碼而理解原來的設計。重構很像是在整理代碼,你所做的就是讓所有東西回到應處的位置上。代碼結構的流失通常是累積性的,因此經常性的重構可以幫助代碼維持自己該有的形態。
1.3.2 重構使軟件更容易理解
其實寫程序,就是和計算機在交談:你編寫代碼告訴計算機做什么事,它的響應則是精確按照你的指示行動。當然除了計算機外,你的源碼還有其他閱讀者:未來可能會有另一位程序員嘗試讀懂你的代碼并做一些修改。我們寫代碼的時候很容易忘記這位未來的程序員,但他才是最重要的。如果一個程序員不理解你的代碼,可能需要很長的時間來修改代碼——而他理解了你的代碼,這個修改或許只需要花費一個小時。
其實很多時候那個未來修改你程序的開發者就是你自己。此時重構就顯得尤其重要。利用重構可以協助你理解不夠熟悉的代碼,而且重構會把你帶到更高的層次上。
1.3.3 重構幫忙找到bug
對代碼的理解,可以更好的找到bug。如果對代碼進行重構,就可以深入理解代碼的功能,搞清楚程序結構的同時,就更容易找出程序的bug。
1.3.4 重構提高編程速度
其實前面提到的一切都歸結到最后一點:重構幫你更快速的開發程序。
聽起來有點兒違反直覺。可以看出重構能夠提高質量。如改善設計、提升可讀性、減少錯誤,這些都是提高質量。但這難道不會降低開發速度嗎?
良好的設計是快速開發的根本——事實上,擁有良好的設計才可能做到快速開發。如果沒有良好的設計,或許某一段時間內你的進展迅速,但是不好的設計很快會讓你的速度慢下來。時間最終都浪費在調試上面。因為你必須花更多的時間去理解系統、尋找重復代碼。如此的循環下去。
良好的設計是維持軟件開發速度的根本。重構可以幫助你更快速地開發軟件,因為重構可以提高設計質量,避免重復的工作。
1.4 何時重構
三次法則 第一次只管做,第二次會產生反感,第三次就應該重構
添加功能時重構 當給軟件添加新特性不方便時候,就應該重構
修補錯誤時重構 對代碼進行重構,可以更方便的發現程序中的bug
復審代碼時重構 重構可以幫助復審別人的代碼
2 代碼的壞味道
如果一段代碼是不穩定或者有一些潛在問題,那么代碼往往會包含一些明顯的痕跡。正如食物要腐壞之前,經常會發出一些異味一樣。Martin Fowler把有問題的代碼稱為“代碼的壞味道”。接下來本文對22種代碼的壞味道進行整理。
2.1 重復代碼
Duplicated Code ——————— (重復代碼) 難維護。
解決方法:提取公共函數。
2.2 過長函數
Long Method ————————–(過長函數) 難理解
解決方法:拆分成若干函數
2.3 過大的類
Large Class—————————-(過大的類) 難用、難理解
解決辦法:拆分成若干類,過程中若遇到Duplicated Code,就提取公共函數。
2.4 過長參數列表
Long parameter List——————(參數多)調用難
解決方法:將參數封裝成結構或者類
2.5 發散式變化
Divergent Change———————(發散式變化)發散式修改,改好多需求,都會動它。
解決方法:拆,將總是一起變化的東西放在一塊兒。
2.6 霰彈式修改
Shotgun Surgery———————(霰彈式修改)散彈式修改,改某個需求時,都會動他。
解決方法:將各個修改點,集中起來,抽象成一個類。
2.7 依戀情結
Feature Envy———————(紅杏出墻的函數)使用了大量其它類的成員。
解決方法:將這個函數挪到那個類里面。
2.8 數據泥團
Data Clumps———————(數據團)。
解決方法:他們那么有基情,就在一起吧,給他們一個新的類。
2.9 基本類型偏執
Primitive Obsession———————(偏愛基本類型) 熱衷于使用int,double等基本類型。
解決方法:反復出現的一組參數,有關聯的多個數組換成類。
2.10 Switch驚悚現身
Switcth Statements———————(switch 語句)。
解決方法:state/strategy或者時簡單的多態。
2.11 平行繼承體系
Parallel Inheritance Hierarchies———————(平行繼承)增加A類的子類ax,B類也需要相應的增加一個bx。
解決方法:應該有一個類時可以去掉繼承關系的。
2.12 冗贅類
Lazy Class———————(冗贅類) 如果他不干活了,炒掉他吧。
解決方法:把這些不再重要的類里面的邏輯,合并到相關類,刪掉舊的。
Speculative Generality———————(夸夸其談未來性)。
解決方法:刪掉。
2.13 令人迷惑的暫時字段
Temporary Field———————(臨時字段)發散式修改,改好多需求,都會動他。
解決方法:將這些臨時變量集中到一個新類中管理。
2.14 過度耦合的消息鏈
Message Chains———————(消息鏈) 過度耦合才是壞的。
解決方法:拆函數或移動函數。
2.15 中間人
Middle Man———————(中間人) 大部分都交給中間人來處理了。
解決方法:用繼承替代委托。
2.16 狎昵關系
Inappropriate Intimacy———————(太親密) 相似的類,有不同接口。
解決方法:劃清界限拆散,或合并,或改成單項聯系。
2.17 異曲同工的類
Alternative Classes with Different Interfaces———————(相似的類,有不同接口)。
解決方法:重命名、移動函數、或抽象子類。
2.18 不完美的類庫
Incomplete Lirary Class———————(不完美類庫)。
解決方法: 包一層函數或包成新的類。
2.19 純稚的數據類
Data Class———————(純數據類)類很簡單,僅有公共成員變量,或簡單操作函數。
解決方法:將相關操作封裝進去,較少public成員變量。
2.20 被拒絕的遺贈
Refused Bequest———————(繼承過多) 父類里面方法很多,子類只用有限幾個。
解決方法:用代理替代繼承關系。
2.21 過多的注釋
Comments———————(太多注釋)這里指代碼太難動了,不得不用注釋解釋。
解決方法:避免用注釋解釋代碼,而是說明代碼的目的,背景等。好代碼會說話。
3 重構方法舉例
3.1 重構函數
3.1.1 重復代碼
這種情況應該很多人都遇到過,編程過程中要盡量避免重復的代碼,解決方法是將重復的內容提煉到一個單獨的函數中。
void A() {.....System.out.println("name" + _name); }void B() {.....System.out.println("name" + _name); }將代碼更改為↓
void A() { .... }void B() { .... }void printName(String name) {System.out.println("name" + name); }3.1.2 內聯臨時變量
如果你對一個變量只使用了一次,那就不妨對它進行一次重構。
int basePrice = order.basePrice(); return (basePrice > 100);更改為↓
return (order.basePrice() > 1000);3.1.3 盡量去掉臨時變量
臨時變量多了會難以維護,所以盡量去掉所使用的臨時變量。
int area = _length * _width; if (area > 1000) return area * 5; elsereturn area *4;更改為↓
if (area() > 1000) return area() * 5; elsereturn area() *4;int area() {return _length * _width; }3.1.4 引入解釋性變量
跟上面那個相反,如果使用函數變得很復雜,可以考慮使用解釋型變量了。
if ((platform.toUpperCase().indexOf("mac") > -1) &&(brower.toUpperCase().indexOf("ie") > -1) &&wasInitializes() && resize > 0) {......}更改為↓
final boolean isMacOS = platform.toUpperCase().indexOf("mac") > -1; final boolean isIEBrowser = brower.toUpperCase().indexOf("ie") > -1; final boolean wasResized = resize > 0;if (isMacOS && isIEBrowser && wasInitializes() && wasResized) {...... }3.1.5 移除對參數的賦值
參數傳入函數中,應該盡量避免對其進行更改。
int discount (int inputVal, int quantity, int yearToDate) {if (inputVal > 50) inputVal -= 2; }更改為↓
int discount (int inputVal, int quantity, int yearToDate) {int result = inputVal;if (result > 50) result -= 2; }另外,函數中聲明的臨時變量最好只被賦值一次,如果超過一次就考慮再聲明變量對其進行分解了。
一個函數也不應該太長,如果太長首先影響理解,其次包含的步驟太多會影響函數復用。做法是將里面的步驟提取為很多小函數,并且函數命名要體現出函數做了什么,清晰明了。
3.2 重構類
3.2.1 搬移方法
每一個方法應該放在最適合的位置,不能隨便亂放,所以很多時候你需要考慮,一個方法在這里是不是最適合的。
class Class1 {aMethod(); }class Class2 { }更改為↓
class Class1 { }class Class2 {aMethod(); }3.3 搬移字段
每一個字段,變量都應該放到其自己屬于的類中,不能隨便放,不屬于這個類中的字段也需要移走。
class Class1 {aField; }class Class2 { }更改為↓
class Class1 { }class Class2 {aField; }3.4 提煉一個新類
將不屬于這個類中的字段和方法提取到一個新的類中。所以說在你寫代碼的時候一定要考慮放這里是不是合適,有沒有其他更合適的地方?
提煉到新的類中↓
3.5 簡化條件表達式
3.5.1 分解條件表達式
有時候看著一個if else語句很復雜,我們就試著把它分解一下。
class Person {private String name;private String officeAreaCode;private String officeNumber;public String getTelephoneNumber() { ..... } }更改為↓
class TelephoneNumber {private String areaCode;private String number;public String getTelephoneNumber() { ..... } }class Person {private String name;private TelephoneNumber _officeNumber; }當然實際情況可能復雜的多,這樣的重構才顯得有意思,這里只是讓大家腦子里有一個這樣的思想,以后遇見這樣的情況能想起來可以這樣子重構。
3.5.2 分解條件表達式
有時我們寫的多個if語句是可以合并到一起的。
if (isUp(case) || isLeft(case)) num = a * b; else num = a * c;更改為↓
if (isTrue(case)) numberB(a); else numberC(a);boolean isTrue(case) {return isUp(case) || isLeft(case); }int numberB(a) {return a + b; }int numberC(a) {return a + c; }3.5.3 合并重復的條件片段
有時候你可能會在if else 語句中寫重復的語句,這時候你需要將重復的語句抽出來。
if (isSpecialDeal()) {total = price * 0.95;send(); } else {total = price * 0.98;send(); }更改為↓
if (isSpecialDeal())total = price * 0.95; elsetotal = price * 0.98;send();3.5.4 以衛句取代嵌套表達式
這個可能有點難以理解,但是我感覺用處還是比較大的,就是加入return語句去掉else語句。
if (a > 0) result = a + b; else {if (b > 0) result = a + c;else {result = a + d;} } return result;更改為↓
if (a > 0) return a + b; if (b > 0) return a + c; return a + d;是不是變得很簡單,加入衛語句就是合理使用return關鍵字。有時候反轉條件表達式也能簡化if else語句。
3.5.5 以多態取代switch語句
這個我感覺很重要,用處非常多,以后你們寫代碼的時候只要碰到switch語句就可以考慮能不能使用面向對象的多態來替代這個switch語句呢?
int getArea() {switch (_shap)case circle:return 3.14 * _r * _r; break;case rect;return _width + _heigth; }更改為↓
class Shap {int getArea(){}; }class Circle extends Shap {int getArea() {return 3.14 * _r * _r; break;} }class Rect extends Shap {int getArea() {return _width + _heigth;} }然后在調用的時候只需要調用Shap的getArea()方法就行了,就可以去掉switch語句了。然后我們還可以在一個方法中引入斷言,這樣可以保證函數調用的安全性,讓代碼更加健壯。
4 總結
文中只是把常用到的,比較好表述的重構方法或情況總結了一下,并沒有覆蓋到書中的所有情況,如果對重構非常有興趣的話建議大家閱讀原書。
總結
以上是生活随笔為你收集整理的重构—改善既有代码的设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用 Python 做数据处理必看:12
- 下一篇: 监督学习——通用线性模型