close
文章出處

轉自:人月神話

重構是對軟件內部結構的一種調整,目的是在不改變軟件之可察性前提下,提高其可理解性,降低其修改成本。關于重構的至理明言如下:

  • 任何一個傻瓜都能寫出計算器可以理解的代碼,唯有寫出人類容易理解的代碼,才是優秀的程序員;
  • 事不過三,三則重構;
  • 當你接獲bug提報,請先撰寫一個單元測試來揭發這個bug;
  • 當你感覺需要撰寫注釋,請先嘗試重構,試著讓所有的注釋變得多余;
  • 當你發現自己需要為程序增加一個特性,而代碼結構使你無法方便的這樣做,就先重構那個程序;
  • 重構之前,必須建立一套可靠的測試機制;
  • 寫軟件就像種樹,優秀的程序員挖成小坑后隨及填好,繼續挖下一個,只會產生一系列小坑,不會有大坑,菜鳥則不會意識到所挖的坑正在變大,還是不停的挖,直到自己掉進大坑,爬不出來,陷入無盡的痛苦深淵;
  • 開發時間越長,越能體會垃圾代碼的痛苦,卻不知道如何改進;
  • Kent Beck:我不是一個偉大的程序員,我只是個有著一些優秀習慣的好程序員而已;


變量(Variable)

不要定義一個臨時變量多次重復使用,臨時變量定義仍然應該可以自解釋,從變量名稱能夠很好的理解變量的含義和作用。在定義一個臨時變量后需要有一段業務邏輯才能夠完成對臨時變量的賦值的時候,可以考慮將這段邏輯抽取到一個獨立的方法。

    double getPrice() {
        int basePrice = _quantity * _itemPrice;
        double discountFactor;
        if (basePrice > 1000) discountFactor = 0.95;
        else discountFactor = 0.98;
        return basePrice * discountFactor;
    }

重構為:

    double getPrice() {
        return basePrice() * discountFactor();
    }

    private int basePrice() {
        return _quantity * _itemPrice;
    }

    private double discountFactor() {
        if (basePrice() > 1000) return 0.95;
        else return 0.98;
    }

當遇到復雜的表達式的時候,需要引入解釋變量,因為復雜的表達式很難進行自解釋。

if ( (platform.toUpperCase().indexOf("MAC") > -1) &&
    (browser.toUpperCase().indexOf("IE") > -1) &&
     wasInitialized() && resize > 0 )
{
      // do something
}

重構為:

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 something
}

減少對全局變量的使用,第一個是全局變量的生命周期很難控制,資源本身無法得到很快的釋放,其二是過多使用全局變量導致在調用方法的時候很難完全清楚方法說需要的入口數據信息,其三,多處都可以對全局變量賦值,我們很難立刻定位到當前全局變量的值來源自哪里?

分解方法(Extract Method)

一個較大的方法往往也會分為多個小的段落,step1,step2,step3,在每一個步驟都會考慮添加注釋說明。而這些相對較為獨立的步驟就可以分解為不同的方法,在分解后方法名可以自解釋方法的功能而不再需要額外的注釋。在一個類里面如果方法里面有一段代碼在多個方法中重復出現,需要抽取該類的公用方法。在多個不同的類中有一段代碼重復出現,需要考慮將公用代碼放到公用類中形成公用方法。

方法名需要很好的自解釋方法的功能,方法的返回盡量單一,方法的入口參數太多的時候應該考慮使用集合,結構或數據對象進行參數的傳遞。參數的傳遞可能出傳遞的是引用,但不要去修改入口參數的值。

不要因為一個方法里面只有一行,兩行很短而不考慮去分解,分解的時候更多的是考慮代碼的自解釋性。代碼本身不是解釋的技術實現機制,而是解釋的業務規則和需求。如果代碼不是解釋的業務規則和需求,那么其它人員就很難快速理解。

引入方法對象來取代方法,當發現一個方法只用到該類里面的幾個關鍵屬性,方法和類里面其它的方法交互很少,輸出單一。由于該方法和這幾個屬性內聚性很強而和該類其它部分松耦合,因此可以考慮將方法和這部分屬性移出形成一個單獨的方法對象。

移動方法,類的職責要單一,一個類的方法更多用到了別的類的屬性,這個方法可能更適合定義在那個類中。

class Account...
   private AccountType _type;
   private int _daysOverdrawn;
   
   double overdraftCharge() {
       if (_type.isPremium()) {
           double result = 10;
           if (_daysOverdrawn > 7) result += (_daysOverdrawn - 7) * 0.85;
           return result;
       }
       else return _daysOverdrawn * 1.75;
   }

   double bankCharge() {
       double result = 4.5;
       if (_daysOverdrawn > 0) result += overdraftCharge();
       return result;
   }

重構為:

class Account...
   private AccountType _type;
   private int _daysOverdrawn;    
   double overdraftCharge() {
       return _type.overdraftCharge(_daysOverdrawn);
   }

   double bankCharge() {
       double result = 4.5;
       if (_daysOverdrawn > 0)
           result += _type.overdraftCharge(_daysOverdrawn);
       return result;
   }

class AccountType...
   double overdraftCharge(Account account) {
       if (isPremium()) {
           double result = 10;
           if (account.getDaysOverdrawn() > 7)
              result += (account.getDaysOverdrawn() - 7) * 0.85;
           return result;
       }
       else return account.getDaysOverdrawn() * 1.75;
   }

分解類(Extract Class)

類的職責的劃分不容易在初次設計時就準確把握,所以在編碼時重構是必要的。職責定位不清!——典型特征是擁有太多的成員變量;而在這里面最重要的就是職責要單一,屬性和方法是否合適的類中。如果不是就需要考慮分解或合并,擴展類的功能,或者抽象相應的接口。面向對象的設計原則如下:

1.單一職責原則 (SRP) - 就一個類而言,應該僅有一個引起它變化的原因

類的職責要單一,類里面的方法只做類的職責范圍里面的事情。MVC即是一種粗粒度的職責話費,模型類重點是提供數據,控制類重點是處理業務邏輯,而V視圖類則是關注數據獲取后的呈現。

數據和數據操作可以考慮分解,如形成專門的DTO數據傳輸對象類。界面類和界面數據提供類也可以考慮分離,如形成專門的Facade層專門負責數據的準備和形成。界面層不應該有太多的數據處理操作。

當發現一個大的類里面的屬性和方法存在明細的分組特性的時候,而且分組直接松散耦合,需要考慮分解為多個類。

引入方法對象來取代方法,當發現一個方法只用到該類里面的幾個關鍵屬性,方法和類里面其它的方法交互很少,輸出單一。由于該方法和這幾個屬性內聚性很強而和該類其它部分松耦合,因此可以考慮將方法和這部分屬性移出形成一個單獨的方法對象。

胖接口也是違反職責單一,胖接口會導致所有實現接口的類都Override所有的接口方法,而有些接口方法往往是子類并不需要的。因此對于胖接口仍然要從職責的角度對接口進行拆分。

2.開放——封閉原則 (OCP)- 對擴展開放,對修改封閉

當發生變化時,只需要添加新的代碼,而不必改動已經正常運行的代碼:軟件人的夢想!而要達到這個目的,關鍵是要能夠較為準確的預測業務變化會導致的可能會發送變化的模塊或代碼。

3.Liskov替換原則 (LSP)

子類型必須能夠替換掉他們的基類型。正是子類型的可替換性,才使得使用基類類型的軟件無須修改就可以擴展。案例參考正方形駁論。矩形的合理假設:長、寬可以獨立變化;而正方形的合理假設:長、寬始終相等。因此正方形并不能從矩形繼承。

4.依賴倒置原則 (DIP) - 高層模塊不應該依賴于低層模塊;抽象不應該依賴于細節。
軟件開發-重構

依賴倒置原則的重點是高層模塊類不要去依賴底層模塊的類,而應該去依賴接口,特別是當我們預見到底層模塊的類本身可能會擴展和變化的時候。這樣在變化的時候最大的好處就是高層類和接口不用變化。

類是否考慮抽象為接口,一方面是根據LSP原則進行重構,一方面是需要觀察我們建立的類,是否有多個類本身存在相同的行為或方法,如果存在則需要考慮抽象接口。


不含病毒。www.avast.com
arrow
arrow
    全站熱搜

    AutoPoster 發表在 痞客邦 留言(0) 人氣()