《重構(gòu):改善既有代碼的設(shè)計》讀書筆記

一,重構(gòu)熊咽,第一個案例

這一章作者先用一個影片出租程序的案例,來演示重構(gòu)的過程


重構(gòu)前UML.PNG
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圖:


作者的重構(gòu).PNG

做了幾點總結(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)增加一個子類。例如

蠟筆.PNG

蠟筆有大中小三種型號藻烤,12種顏色绷雏,總共必須有36種蠟筆头滔。
每增加一種顏色,都必須增加大中小三種型號涎显。顏色型號緊緊耦合在一起坤检。

再來看毛筆,不同的毛筆型號抽象成毛筆期吓,不同顏色抽象成顏料早歇,
毛筆和顏料兩個基類形成關(guān)聯(lián),避免了Shortgun Surgery讨勤,這就是Bridge模式箭跳。

毛筆.PNG

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)系

hide delegate.PNG

6 . Remove Middle Man(移除中間人)
封裝委托對象也是要付出代價的:每當(dāng)客戶要使用受托類的新特性時矾利,就必須在服務(wù)端添加一個委托函數(shù)梦皮。
隨著委托類的特性(功能)越來越多剑肯,服務(wù)類完全變成了“中間人”,此時就應(yīng)該讓客戶直接調(diào)用受托類呀忧。
很難說什么程度的隱藏才是合適的而账,隨著系統(tǒng)不斷變化泞辐,使用Hide DelegateRemove 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ù)。讓這個擴展品成為源類的子類戒包裝類咕别。

introduce local extension.PNG

八惰拱、重新組織數(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)

change value to reference.PNG

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ù)。

Replace Conditional with Polymorphism.PNG

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)用另一個。

12-1.PNG

2 . Convert Procedural design to Objects 將過程化設(shè)計轉(zhuǎn)化為對象設(shè)計
你手上有一些傳統(tǒng)過程風(fēng)格的代碼 译隘。
將數(shù)據(jù)記錄變成對象亲桥,將大塊的行為分成小塊,并將行為移入相關(guān)對象之中细燎。

12-2.PNG

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逆害、壞味道等等)头镊。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市魄幕,隨后出現(xiàn)的幾起案子相艇,更是在濱河造成了極大的恐慌,老刑警劉巖纯陨,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坛芽,死亡現(xiàn)場離奇詭異留储,居然都是意外死亡,警方通過查閱死者的電腦和手機咙轩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門获讳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人活喊,你說我怎么就攤上這事丐膝。” “怎么了钾菊?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵帅矗,是天一觀的道長。 經(jīng)常有香客問我煞烫,道長浑此,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任滞详,我火速辦了婚禮凛俱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘茵宪。我一直安慰自己最冰,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布稀火。 她就那樣靜靜地躺著,像睡著了一般赌朋。 火紅的嫁衣襯著肌膚如雪凰狞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天沛慢,我揣著相機與錄音赡若,去河邊找鬼。 笑死团甲,一個胖子當(dāng)著我的面吹牛逾冬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播躺苦,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼身腻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了匹厘?” 一聲冷哼從身側(cè)響起嘀趟,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎愈诚,沒想到半個月后她按,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牛隅,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年酌泰,在試婚紗的時候發(fā)現(xiàn)自己被綠了媒佣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡陵刹,死狀恐怖丈攒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情授霸,我是刑警寧澤巡验,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站碘耳,受9級特大地震影響显设,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辛辨,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一捕捂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧斗搞,春花似錦指攒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至虑啤,卻和暖如春隙弛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狞山。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工全闷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人萍启。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓总珠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親勘纯。 傳聞我的和親對象是個殘疾皇子局服,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容