《重構 改善既有代碼的設計》讀書筆記

讀書筆記

《重構 改善既有代碼的設計》

本文github地址:
https://github.com/YoungBear/MyBlog/blob/master/refactor.md

重構:在不改變軟件可觀察行爲的前提下改善其內部結構。

refactoring

tips:

  如果你發現自己需要爲程序添加一個特性,而代碼結構使你無法很方便地達成目的,那就先重構那個程序,使特性的添加比較容易進行,然後再添加特性。

  重構前,先檢查自己是否有一套可靠的測試機制。這些測試必須有自我檢驗能力。

  重構技術就是以微小的步伐修改程序。如果你犯下錯誤,很容易便可發現它。

  任何一個傻瓜都能寫出計算機可以理解的代碼。唯有寫出人類容易理解的代碼,纔是優秀的程序員。(變量命名)

  重構(名詞):對軟件內部結構的一種調整,目的是在不改變軟件可觀察行爲的前提下,提高其可理解性,降低其修改成本。

  重構(動詞):使用一系列重構方法,在不改變軟件可觀察可觀察行爲的前提下,調整其結構。
  

爲何重構

  1. 重構改進軟件設計
  2. 重構使軟件更容易理解
  3. 重構幫助找到Bug
  4. 重構提高編程速度

何時重構

三次法則

第一次做某件事時只管去做;第二次做類似的事會產生反感,但無論如何還是可以去做;第三次再做類似的事,你就應該重構。

tips:

事不過三,三則重構。

  1. 添加新功能時重構
  2. 修補錯誤時重構
  3. 複審代碼時重構

第3章 代碼的壞味道

  1. Duplicated Code 重複代碼
  2. Long Method 過長函數
  3. Large Class 過大的類
  4. Long Parameter List 過長參數列
  5. Divergent Change 發散式變化 – 軟件能夠更容易被修改
  6. Shotgun Surgery 霰彈式修改 – 在很多類中做出許多小的修改
  7. Feature Envy 依戀情結 – 函數對某個類的興趣高過對自己所處類的興趣
  8. Data Clumps 數據泥團 – 很多相同的參數(可以新建一個類來保存)
  9. Primitive Obsession 基本類型偏執
  10. Switch Statements switch 驚悚現身
  11. Parallel Inheritance Hierarchies 平行繼承體系
  12. Lazy Class 冗贅類
  13. Speculative Generality 誇誇其談未來性
  14. Temporary Field 令人迷惑的暫時字段
  15. Message Chains 過度耦合的消息鏈
  16. Middle Man 中間人
  17. Inappropriate Intimacy 狎暱關係
  18. Alternative Classes with Different Interfaces 異曲同工的類
  19. Incomplete Library Class 不完美的類庫
  20. Data Class 純稚的數據類
  21. Refused Bequest 被拒絕的遺贈
  22. Comments 過多的註釋

第6章 重新組織函數

6.1 Extract Method 提煉函數

6.2 Inline Method 內聯函數

6.3 Inline Temp 內聯臨時變量

6.4 Replace Temp with Query 以查詢取代臨時變量

6.5 Introduce Explaining Variable 引入解釋性變量

6.6 Split Temporary Variable 分解臨時變量

  如果某一個臨時變量被賦值超過一次,並且它既不是循環變量也不是用於蒐集計算結果則針對每次賦值,創造一個獨立、對應的臨時變量

做法:

  1. 將新的臨時變量聲明爲final
  2. 以該臨時變量的第二次賦值動作爲界,修改此前對該臨時變量的所有引用點,讓它們引用新的臨時變量
  3. 在第二次賦值處,重新聲明原先那個臨時變量
  4. 編譯,測試
  5. 逐次重複上述過程。每次都在聲明處對臨時變量改名,並修改下次賦值之前的引用點

6.7 Remove Assignments to Parameters 移除對參數的賦值

做法:

  1. 建立一個臨時變量,把待處理的參數值賦予它。
  2. 以“對參數的賦值”爲界,將其後所有對此參數的引用點,全部替換爲“對此臨時變量的引用”。
  3. 修改賦值語句,使其改爲對新建之臨時變量賦值。
  4. 編譯,測試。

6.8 Replace Method with Method Object 以函數對象取代函數

  如果一個函數中局部變量氾濫成災,那麼想分解這個函數是非常困難的。以查詢取代臨時變量可以助你減輕這個負擔,但有時候你會發現根本無法拆解一個需要拆解的函數。這種情況下,你應該把手伸進工具箱的深處,祭出函數對象這件法寶。

做法:

  1. 建立一個新類,根據待處理函數的用途,爲這個類命名。
  2. 在新類中建立一個final字段,用以保存原先大型函數所在的對象。我們將這個字段成爲“源對象”。同時,針對原函數的每個臨時變量每個參數,在新類中建立一個對應的字段保存之。
  3. 在新類中建立一個構造函數,接收源對象及原函數的所有參數作爲參數。
  4. 在新類中建立一個compute()函數。
  5. 將原函數的代碼複製到compute()函數中。如果需要調用源對象的任何函數,請通過源對象字段調用。
  6. 編譯。
  7. 將舊函數的函數本體替換爲這樣一條語句:“創建上述新類的一個新對象,而後調用其中的compute()函數”。

     這項重構的好處是:我們可以輕鬆地對compute()函數採取Extract Method(提煉函數),不必擔心參數傳遞的問題。
     

6.9 Substitute Algorithm 替換算法

把某一個算法替換爲另一個更清晰的算法。

第7章 在對象之間搬移特性

7.1 Move Method (搬移函數)

你的程序中,有個函數與其所駐類之外的另一個類進行更多交流:調用後者,或者被後者調用。

思路:
在該函數最常引用的類中建立一個有着類似行爲的新函數。將舊函數變成一個單純的委託函數,或是將舊函數完全移除。

7.2 Move Field (搬移字段)

你的程序中,某個字段被其所駐類之外的另一個類更多地用到。

思路:在目標類新建一個字段,修改源字段的所有用戶,令它們改用新字段。

7.3 Extract Class (提煉類)

某各類做了應該由兩個類做的事。

思路:建立一個新類,將相關的字段和函數從舊類搬移到新類。

7.4 Inline Class (將類內聯化)

某個類沒有做太多事情。

思路:將這個類的所有特性搬移到另一個類中,然後移除原類。

7.5 Hide Delegate (隱藏“委託關係”)

客戶通過一個委託類來調用另一個對象。

思路:在服務類上建立客戶所需求的所有函數,用以隱藏委託關係。

7.6 Remove Middle Man (移除中間人)

某個類做了過多的簡單委託動作。

思路:讓客戶直接調用受託類。(和7.5剛好相反)

7.7 Introduce Foreign Method (引入外加函數)

你需要爲提供服務的類增加一個函數,但你無法修改這個類。

思路:在客戶類中建立一個函數,並以第一參數形式傳入一個服務類實例。

7.8 Introduce Local Extension (引入本地擴展)

你需要爲服務類提供一些額外函數,但你無法修改這個類。

思路:建立一個新類,使它包含這些額外函數。讓這個擴展品成爲源類的子類或包裝類。

第8章 重新組織數據

8.1 Self Encapsulate Filed (自封裝字段)

將屬性聲明爲private,使用get/set函數來訪問。

8.2 Replace Data Value with Object (以對象取代數據值)

你有一個數據項,需要與其他數據和行爲一起使用纔有意義。

思路:將數據項變成對象。

8.3 Change Value to Reference (將值對象改爲引用對象)

你從一個類衍生出許多彼此相等的實例,希望將它們替換爲同一個對象。

思路:將這個值對象變成引用對象。

8.4 Change Reference to Value (將引用對象改爲值對象)

你有一個引用對象,很小且不可變,而且不易管理。

思路:將它變成值對象。

8.5 Replace Array with Object (以對象取代數組)

你有一個數組,其中的元素各自代表不同的東西。

思路:以對象替換數組,其中的數組中的每個元素,以一個字段來表示。

8.6 Duplicate Observed Data (複製“被監視數據”)

你有一些領域數據置身於GUI控件中,而領域函數需要訪問這些數據。

思路:將該數據複製到一個領域對象中。建立一個Observer模式,用以同步領域對象和GUI對象內的重要數據。

8.7 Change Unidirectional Association to Bidirectional (將單向關聯改爲雙向關聯)

兩個類都需要使用雙方特性,但其間只有一條單向連接。

思路:添加一個反向指針,並使修改函數能夠同時更新兩條連接。

8.8 Change Bidirectional Association to Unidirectional (將雙向關聯改爲單向關聯)

兩個類之間有雙向關聯,但其中一個類如今不再需要另一個類的特性。

思路:去除不必要的關聯。

8.9 Replace Magic Number with Symbolic Constant (以字面常量取代魔法數)

你有一個字面數值,帶有特別含義。

思路:創造一個常量,根據其意義爲它命名,並將上述的字面數值替換爲這個常量。

eg. 使用PI來代替3.14

8.10 Encapsulate Field (封裝字段)

你的類中存在一個 public 字段。

思路:將它聲明爲 private, 並提供相應的訪問函數。

8.11 Encapsulate Collection (封裝集合)

有一個函數返回一個集合。

思路:讓這個函數返回該集合的一個只讀副本,並在這個類中提供添加/移除集合元素的函數。

動機:

我們常常會在一個類中使用集合(collection,可能是array,list,set或vector)來保存一組實例。這樣的類通常也會提供指針對該集合的取值/設值函數。

但是,集合的處理方式應該和其他種類的數據略有不同。取值函數不該返回集合自身,因爲這會讓用戶得以修改集合內容而集合擁有者卻一無所悉。這也會對用戶暴露過多對象內部數據結構的信息。如果一個取值函數確實需要返回多個值,它應該避免用戶直接操作對象內所保存的集合,並隱藏對象內與用戶無關的數據結構。至於如何做到這一點,視你使用的 Java 版本不同而有所不同。

另外,不應該爲這整個集合提供一個設值函數,但應該提供用以爲集合添加/移除元素的函數。這樣,集合擁有者(對象)就可以控制元素的添加和移除。

如果你做到以上幾點,集合就可以很好地封裝起來了,這便可以降低集合擁有者和用戶之間的耦合度。

8.12 Replace Record with Data Class (以數據類取代記錄)

你需要面對傳統編程環境中的記錄結構。

思路:爲該記錄創建一個“啞”數據對象

8.13 Replace Type Code with Class (以類取代類型碼)

類之中有一個數值類型碼,但它並不影響類的行爲。

思路:以一個新的類替換該數值類型碼

8.14 Replace Type Code with Subclasses (以子類取代類型碼)

你有一個不可變的類型碼,它會影響類的行爲。

思路:以子類取代這個類型碼

8.15 Replace Type Code with State/Strategy (以 State/Strategy 取代類型碼)

你有一個類型碼,它會影響類的行爲,但你無法通過集成手法消除它。

思路:以狀態對象取代類型碼

8.16 Replace Subclass with Fields (以字段取代子類)

你的各個子類的唯一差別只在“返回常量數據”的函數身上。

思路:修改這些函數,使它們返回超類中的某個(新增)字段,然後銷燬子類。

動機:

建立子類的目的,是爲了增加新特性或變化其行爲。有一種變化行爲被稱爲“常量函數(constant method)”,它們會返回一個硬編碼的值。這東西有其用途:你可以讓不同的子類中的同一個訪問函數返回不同的值。你可以在超類中將反問函數聲明爲抽象函數,並在不同的子類中讓它返回不同的值。

儘管常量函數有其用途,但若子類只有常量函數,實在沒有足夠的存在價值。你可以在超類中設計一個與常量函數返回值相應的字段,從而完全去除這樣的子類。如此一來就可以避免因繼承而帶來的額外複雜性。

第9章 簡化條件表達式

9.1 Decompose Conditional (分解條件表達式)

你有一個複雜的條件 (if-then-else) 語句。

思路:從 if,then,else 三個段落中分別提煉出獨立函數。

動機

程序之中,複雜的條件邏輯是最常導致複雜度上升的地點之一。你必須編寫代碼來檢查不同的條件分支、根據不同的分支做不同的事,然後,你很快就會得到一個相當長二代函數。大型函數自身就會使代碼的可讀性下降,而條件邏輯則會使代碼更難閱讀。在帶有複雜條件邏輯的函數中,代碼(包括檢查條件分支的代碼和真正實現功能的代碼)會告訴你發生的事,但常常讓你弄不清楚爲什麼會發生這樣的事,這就說明代碼的可讀性的確大大降低了。

和任何大塊頭代碼一樣,你可以將它分解爲多個獨立函數,根據每個小塊代碼的用途,爲分解而得的新函數命名,並將原函數中對應的代碼改爲調用新建函數,從而更清楚地表達自己的意圖。對於條件邏輯,將每個分支條件分解成新函數還可以給你帶來更多好處:可以突出條件邏輯,更清楚地表達每個分支的作用,並且突出每個分支的原因。

做法

  • 將 if 段落提煉出來,構成一個獨立函數。
  • 將 then 段落和 else 段落都提煉出來,各自構成一個獨立函數。

9.2 Consolidate Conditional Expression (合併條件表達式)

你有一系列條件測試,都得到相同結果。

思路:將這些測試合併爲一個條件表達式,並將這個條件表達式提煉成爲一個獨立函數。

動機

有時你會發現這樣一串條件檢查:檢查條件各不相同,最終行爲卻一致。如果發現這種情況,就應該使用“邏輯或”和“邏輯與”將它們合併爲一個條件表達式。

之所以要合併條件代碼,有兩個重要原因。首先,合併後的條件代碼會告訴你“實際上只有一次條件檢查,只不過有多個並列條件需要檢查而已”,從而使這一次檢查的用意更清晰。當然, 合併前和合並後的代碼有着相同的效果,但原先代碼傳達出的信息卻是“這裏有一些各自獨立的條件測試,它們只是恰好同時發生”。其次,這項重構往往可以爲你使用 Extract Method 做好準備。將檢查條件提煉成一個獨立函數對於理清代碼意義非常有用,因爲它把描述“做什麼”的語句換成了“爲什麼這樣做”。

條件語句的合併理由也同時指出了不要合併的理由:如果你認爲這些檢查的確彼此獨立,的確不應該被視爲同一次檢查,那麼就不要使用本項重構。因爲在這種情況下,你的代碼已經很清楚表達出自己的意義。

9.3 Consolidate Duplicate Conditional Fragments (合併重複的條件片段)

在條件表達式的每個分支上有着相同的一段代碼。

思路:將這段重複代碼搬移到條件表達式之外

動機:

有時你會發現,一組條件表達式的所有分支都執行了相同的某段代碼。如果是這樣,你就應該將這段代碼搬移到條件表達式外面。這樣,代碼才能更清楚地表明哪些東西隨條件的變化而變化、哪些東西保持不變。

9.4 Remove Control Flag (移除控制標記)

在一系列布爾表達式中,某個變量帶有“控制標記”(control flag) 的作用。

思路:以 break 語句或 return 語句取代控制標記。

9.5 Replace Nested Conditional with Guard Clauses (以衛語句取代嵌套條件表達式)

函數中的條件邏輯使人難以看清正常的執行路徑。

思路:使用衛語句表現所有特殊情況。

eg.

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isReetired) result = retiredAmount();
            else result = normalPayAmount();
        }
    }
    return result;
}

重構之後:

double getPayAmount() {
    if (_isDead) return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired) return retiredAmount();
    return normalPayAmount();
}

動機:

根據我的經驗,條件表達式通常有兩種表現形式。第一種形式是:所有分支都屬於正常行爲。第二種形式是:條件表達式提供的答案中只有一種是正常行爲,其他都不是不常見的情況。

這兩類條件表達式有不同的用途,這一點應該通過代碼表現出來。如果兩條分支都是正常行爲,就應該使用形如 if…else… 的條件表達式;如果某個條件極其罕見,就應該單獨檢查該條件,並在該條件爲真時立刻從函數中返回。這樣的單獨檢查常常被稱爲“衛語句” (guard clauses)。

Replace Nested Conditional with Guard Clauses 的精髓就是:給某一條分支以特別的重視。如果使用 if-then-else 結構,你對 if 分支
和 else 分支的重視是同等的。這樣的代碼結構傳遞給閱讀者的消息就是:各個分支有同樣的重要性。衛語句就不同了,它告訴閱讀者:“這種情況很罕見,如果它真的發生了,請做一些必要的整理工作,然後退出。”

“每個函數只能有一個入口和一個出口”的觀念,根深蒂固於某些程序員的腦海裏。我發現,當我處理他們編寫的代碼時,經常需要使用這項重構。現今的編程語言都會強制保證每個函數只有一個入口,至於“單一出口”規則,其實不是那麼有用。在我看來,保持代碼清晰纔是最關鍵的:如果單一出口能使這個函數更清楚易讀,那麼就使用單一出口;否則就不必這麼做。

嵌套條件代碼往往由那些深信“每個函數只能有一個出口”的程序員寫出。我發現那條規則實在有點太簡單粗暴了。如果對函數剩餘部分不再有興趣,當然應該立刻退出。引導閱讀者去看一個沒有用的else區段,只會妨礙他們的理解。

範例:將條件反轉

我們常常可以將條件表達式反轉,從而實現該項重構。

初始代碼:

public double getAdjustedCapital() {
    double result = 0.0;
    if (_capital > 0.0) {
        if (_intRate > 0.0 && _duration > 0.0) {
            result = (_income / _duration) * ADJ_FACTOR;
        }
    }
    return result;
}

我們將逐一進行替換。不過這次在插入衛語句時,我們需要將相應的條件反轉過來:

public double getAdjustedCapital() {
    double result = 0.0;
    if (_capital <= 0.0) return result;//將這個條件反轉,並使用衛語句(Guard Glauses)
    if (_intRate > 0.0 && _duration > 0.0) {
            result = (_income / _duration) * ADJ_FACTOR;
    }
    return result;
}

下一個條件稍微複雜一點,所以我們分兩步進行逆反。首先加入一個邏輯非操作:

public double getAdjustedCapital() {
    double result = 0.0;
    if (_capital <= 0.0) return result;
    if (!(_intRate > 0.0 && _duration > 0.0)) return result;//加入邏輯非操作,並使用衛語句
    result = (_income / _duration) * ADJ_FACTOR;
    return result;
}

將邏輯非簡化:

public double getAdjustedCapital() {
    double result = 0.0;
    if (_capital <= 0.0) return result;
    if (_intRate <= 0.0 || _duration <= 0.0)) return result;//簡化邏輯非操作
    result = (_income / _duration) * ADJ_FACTOR;
    return result;
}

這時候,我比較喜歡在衛語句內返回一個明確值,因爲這樣我們可以一目瞭然地看到衛語句返回的失敗結果。此外,這種時候我們也會考慮使用 Replace Magic Number with System Constant。

public double getAdjustedCapital() {
    double result = 0.0;
    if (_capital <= 0.0) return 0.0;//在衛語句中返回明確值
    if (_intRate <= 0.0 || _duration <= 0.0)) return 0.0;//在衛語句中返回明確值
    result = (_income / _duration) * ADJ_FACTOR;
    return result;
}

完成替換之後,我們同樣可以將臨時變量移除:

public double getAdjustedCapital() {
    if (_capital <= 0.0) return 0.0;
    if (_intRate <= 0.0 || _duration <= 0.0)) return 0.0;
    return (_income / _duration) * ADJ_FACTOR;
}

9.6 Replace Conditional with Polymorphism (以多態取代條件表達式)

你手上有個條件表達式,它根據對象類型的不同而選擇不同的行爲。

思路:將這個條件表達式的每個分支放進一個子類內的覆寫函數中,然後將原始函數聲明爲抽象函數。

動機

在面向對象術語中,聽上去最高貴的詞非“多態”莫屬。多態最根本的好處就是:如果你需要根據對象的不同類型而採取不同的行爲,多態使你不必編寫明顯的條件表達式。

正因爲有了多態,所以你會發現:“類型碼的 switch 語句”以及“基於類型名稱的 if-then-else 語句” 在面向對象程序中很少出現。

多態能夠給你帶來很多好處。如果同一組條件表達式在程序許多地點出現,那麼使用多態的收益是最大的。使用條件表達式時,如果你想添加一種新類型,就必須查找並更新所有條件表達式。但如果改用多態,只需建立一個新的子類,並在其中提供適當的函數就行了。類的用戶不需要了解這個子類,這就大大降低了系統各部分之間的依賴,使系統升級更加容易。

9.7 Introduce Null Object (引入 Null 對象)

你需要再三檢查某對象是否爲null。

思路:將null值替換爲null對象

9.8 Introduce Assertion (引入斷言)

某一段代碼需要對程序狀態做出某種假設。

思路:以斷言明確表現這種假設。

double getExpenseLimit() {
    //shoule have either expense limit or a primary project
    return (_expenseLimit != NULL_EXPENSE) ?
            _expenseLimit : _primaryProject.getMemberExpenseLimit();
}

這項重構後:

double getExpenseLimit() {
    Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null);
    return (_expenseLimit != NULL_EXPENSE) ?
            _expenseLimit : _primaryProject.getMemberExpenseLimit();
}

動機

常常會有這樣一段代碼:只有當某個條件爲真時,該段代碼才能正常運行。例如平方根計算只對正值才能進行,又例如某個對象可能假設其字段至少有一個不等於null。

這樣的假設通常並沒有在代碼中明確表現出來,你必須閱讀整個算法才能看出。有時程序員會以註釋寫出這樣的假設。而我要介紹的是一種更好的技術:使用斷言明確標明這些假設。

斷言是一個條件表達式,應該總是爲真。如果它失敗,表示程序員犯了錯誤。因此斷言的失敗應該導致一個非受控異常 (unchecked exception)。斷言絕對不能被系統的其他部分使用。實際上,程序最後的成品往往將斷言統統刪除。因此,標記“某些東西是個斷言”是很重要的。

斷言可以作爲交流與調試的輔助。在交流的角度上,斷言可以幫助程序閱讀者理解代碼所做的假設;在調試的角度上,斷言可以在距離bug最近的地方抓住它們。當我編寫自我測試代碼的時候發現,斷言在調試方面的幫助變得不那麼重要了,但我仍然非常看重它們在交流方面的價值。

第10章 簡化函數調用

10.1 Rename Method (函數改名)

函數的名稱未能揭示函數的用途。

思路:修改函數名稱。

10.2 Add Parameter (添加參數)

某個函數需要從調用端得到更多信息。

思路:爲此函數添加一個對象參數,讓該對象帶進函數所需信息。

10.3 Remove Parameter (移除參數)

函數本體不再需要某個參數。

思路:將該參數去除。

10.4 Separate Query from Modifier (將查詢函數和修改函數分離)

某個函數既返回對象狀態值,又修改對象狀態。

思路:建立兩個不同的函數,其中一個負責查詢,另一個負責修改。

10.5 Parameterize Method (令函數攜帶參數)

若干函數做了類似的工作,但在函數本體中卻包含了不同的值。

思路:建立單一函數,以參數表達那些不同的值。

動機:

你可能會發現這樣的兩個函數:它們做着類似的工作,但因少數幾個值致使行爲略有不同。這種情況下,你可以將這些各自分離的函數統一起來,並通過參數來處理那些變化情況,用以簡化問題。這樣的修改可以去除重複的代碼,並提高靈活性,因爲你可以用這個參數處理更多的變化情況。

範例

一個最簡單的例子:

class Employee {
    void tenPercentRaise() {
        salary *= 1.1;
    }

    void fivePercentRaise() {
        salary *= 1.05;
    }
}

這段代碼可以替換如下:

void raise(double factor) {
    salary *= (1 + factor);
}

本項重構的要點在於:以“可將少量數值視爲參數”爲依據,找出帶有重複性的代碼。

10.6 Replace Rarameter with Explicit Methods (以明確函數取代參數)

你有一個函數,其中完全取決於參數值而採取不同行爲

思路:針對該參數的每一個可能值,建立一個獨立函數。

eg.

void setValue(String name, int value) {
    if (name.equals("height")) {
        _height = value;
        return;
    }
    if (name.equals("width")) {
        _width = value;
        return;
    }
    Assert.shouldNeverReachHere();
}

經過這項重構後:

void setHeight(int arg) {
    _height = arg;
}

void setWidth(int arg) {
    _width = arg;
}

動機:

Replace Parameter with Explicit Methods 恰恰相反於 Parameterize Method 。如果某個參數有多種可能的值,而函數內又以條件表達式檢查這些參數值,並根據不同參數值做出不同的行爲,那麼就應該使用本項重構。調用者原本必須賦予參數適當的值,以決定該函數做出何種響應。現在,既然你提供了不同的函數給調用者使用,就可以避免出現條件表達式。此外你還可以獲得編譯期檢查的好處,而且接口也更清楚。如果以參數值決定函數行爲,那麼函數用戶不但需要觀察該函數,而且還要判斷參數值是否合法,而“合法的參數值”往往很少在文檔中被清楚地提出。

就算不考慮編譯期檢查的好處,只是爲了獲得一個清晰的接口,也值得你執行本項重構。哪怕只是給一個內部的布爾變量賦值,相比較之下,Switch.beOn() 也比 Switch.setState(true) 要清楚得多。

但是,如果參數值不會對函數行爲有太多影響,你就不應該使用 Replace Parameter with Explicit Methods。如果情況這是這樣,而你也只需要通過參數爲一個字段賦值,那麼直接使用設置函數就行了。如果的確需要條件判斷的行爲,可考慮使用Replace Conditional with Polymorphism。

做法:

  • 針對參數的每一種可能值,新建一個明確函數。
  • 修改條件表達式的每個分支,使其調用合適的新函數。
  • 修改每個分支後,編譯並測試。
  • 修改原函數的每一個被調用點,改而調用上述的某個合適的新函數。
  • 編譯,測試。
  • 所有調用端都修改完畢後,刪除原函數。

10.7 Preserve Whole Object (保持對象完整)

你從某個對象中取出若干值,將它們作爲某一次函數調用時的參數。

思路:改爲傳遞整個對象。

eg.

int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
withinPlan = plan.withinRange(low, high);

經過這項重構後:

withinPlan = play.withinRange(daysTempRange());

動機:

有時候,你會將來自同一對象的若干項數據作爲參數,傳遞給某個函數。這樣做的問題在於:萬一將來被調用函數需要新的數據項,你就必須查找並修改對此函數的所有調用。如果你把這些數據所屬的整個對象傳給函數,可以避免這種尷尬的處境,因爲被調用函數可以向那個參數對象請求任何它想要的信息。

除了可以使參數列更穩固之外,Preserve Whole Object 往往還能提高代碼的可讀性。過長的參數列很難使用,因爲調用者和被調用者都必須記住這些參數的用途。此外,不使用完整對象也會造成重複代碼,因爲被調用函數無法利用完整對象中的函數來計算某些中間值。

不過事情總有兩面。如果你傳的是數值,被調用函數就只依賴於這些數值,而不依賴它們所數的對象。但如果你傳遞的是整個對象,被調用函數所在的對象就需要依賴參數對象。如果這會使你的依賴結構惡化,那麼就不該使用Preserve Whole Object。

還有一種不使用 Preserve Whole Object 的理由:如果被調用函數只需要參數對象的其中一項數值,那麼只傳遞那個數值會更好。我並不認同這種觀點,因爲傳遞一項數值和傳遞一個對象,至少在代碼清晰度上是等價的(當然對於按值傳遞的參數來說,性能上可能有所差異)。更重要的考量應該放在對象之間的依賴關係上。

如果被調用函數使用了來自另一個對象的很多數據,這可能意味着該函數實際上應該本定義在那些數據所屬的對象中。所以,考慮 Preserce Whole Object 的同時,你也應該考慮Move Method。

運用本項重構之前,你可能還沒定義一個完整對象。那麼就應該先用 Introduce Parameter Object 。

還有一種常見情況:調用者將自己的若干數據作爲參數,傳遞給被調用函數。這種情況下,如果該對象有合適的取值函數,你可以使用this取代這些參數值,並且無需擔心對象依賴問題。

10.8 Replace Parameter with Methods (以函數取代參數)

對象調用某個函數,並將所得結果作爲參數,傳遞給另一個函數。而接受該參數的函數本身也能夠調用前一個函數。

思路:讓參數接受者取出該項參數,並直接調用前一個函數。

eg.

int basePrice = _quantity * _itemPrice;
discountLevel = getDiscountLevel();
double finalPrice = discountedPrice (basePrice, discountLevel);

通過本項重構後:

int basePrice = _quantity * _itemPrice;
double finalPrice = discountedPrice (basePrice);//在discountedPrice內部調用getDiscountLevel()

動機

如果函數可以通過其他途徑獲得參數值,那麼它就不應該通過參數取得該值。過長的參數列會增加程序閱讀者的理解難度,因此我們應該儘可能縮短參數列的長度。

10.9 Introduce Parameter Object(引入參數對象)

某些參數總是很自然地同時出現。

思路:以一個對象取代這些參數。

動機

你經常會看到特定的一組參數總是一起被傳遞。可能有好幾個函數都使用這一組參數,這些函數可能隸屬同一個類,也可能隸屬於不同的類。這樣一組參數就是所謂的 Data Clumps (數據抱團),我們可以運用一個對象包裝所有這些數據,再以該對象取代它們。哪怕只是爲了把這些數據組織在一起,這樣做也是值得的。本項重構的價值在於縮短參數列,而你知道,過長的參數列總是難以理解的。此外,新對象所定義的訪問函數還可以使代碼更具一致性,這又進一步降低了理解和修改代碼的難度。

本項重構還可以帶給你更多好處。當你把這些參數組織到一起之後,往往很快可以發現一些可被移至新建類的行爲。通常,原本使用那些參數的函數對這一組參數會有一些共通的處理,如果將這些共通行爲移到新對象中,你可以減少很多重複代碼。

10.10 Remove Setting Method (移除設值函數)

類中的某個字段應該在對象創建時被設值,然後就不再改變。

思路:去掉該字段的所有設值函數。

動機

如果你爲某個字段提供了設值函數,這就按時這個字段值可以被改變。如果你不希望在對象創建之後此字段還有機會被改變,那就不要爲它提供設值函數 (同時將該字段設爲final)。這樣你的意圖會更加清晰,並且可以排除其值被修改的可能性————這種可能性往往是非常大的。

如果你保留了間接訪問變量的方法,就可能疆場有程序員盲目使用它們。這些人甚至會在構造函數中使用設值函數!我猜想他們或許是爲了代碼的一致性,但卻忽略了設值函數往後可能帶來的混淆。

10.11 Hide Method (隱藏函數)

有一個函數,從來沒有被其他任何類用到。

思路:將這個函數修改爲private。

動機

重構往往促使你修改函數的可見度。提高函數可見度的情況很容易想象:另一個類需要用到某個函數,因此你必須提高該函數的可見度。但是要指出一個函數的可見度是否過高,就稍微困難一些。理想狀況下,你可以使用工具檢查所有函數,指出可被隱藏起來的函數。即使沒有這樣的工具,你也應該時常進行這樣的檢查。

一種特別常見的情況是:當你面對一個過於豐富、提供了過多行爲的接口時,就值得將非必要的取值函數和設值函數隱藏起來。尤其當你面對的是一個只有簡單封裝的數據容器時,情況更是如此。隨着越來越多行爲被放入這個類,你會發現許多取值/設值函數不再需要公開,因此可以把它們隱藏起來。如果你把取值/設值函數設爲private,然後在所有地方都直接訪問變量,那就可一個放心移除取值/設值函數了。

10.12 Replace Constructor with Factory Method (以工廠函數取代構造函數)

你希望在創建對象時不僅僅是做簡單的建構動作。

思路:將構造函數替換爲工廠函數。

動機

使用 Replace Constructor with Factory Method 的最顯而易見的動機,就是在派生子類的過程中以工廠函數取代類型碼。你可能常常需要根據類型碼創建相應的對象,現在,創建名單中還得加上子類,那些子類也是根據類型碼來創建。然而由於構造函數只能返回單一類型的對象,因此你需要將構造函數替換爲工廠函數。

此外,如果構造函數的功能不能滿足你的需要,也可以使用工廠函數來代替它。工廠函數也是 Change Value to Reference 的基礎。你也可以令你的工廠函數根據參數的個數和類型,選擇不同的創建行爲。

10.13 Encapsulate Downcast (封裝向下轉型)

某個函數返回的對象,需要由函數調用者執行向下轉型(downcast)。

思路:將向下轉型動作移到函數中。

eg.

Object lastReading() {
    return readings.lastElement();
}

通過這項重構後:

Reading lastReading() {
    return (Reading) readings.lastElement();
}

10.14 Replace Error Code with Exception (以異常取代錯誤碼)

某個函數返回一個特定的代碼,用以表示某種錯誤情況。

思路:改用異常。

eg.

int withdraw(int amount) {
    if (amount > _balance) {
        return -1;
    } else {
        _balance -= amount;
        return 0;
    }
}

通過這項重構後:

void withdraw(int amount) throws BalanceExceprion {
    if (amount > _balance) {
        throw new BalanceException();
    }
    _balance -= amount;
}

10.15 Replace Exception with Test (以測試取代異常)

面對一個調用者可以預先檢查的條件,你拋出了一個異常。

思路:修改調用者,使它在調用函數之前先做檢查。

eg.

double getValueForPeriod(int periodNumber) {
    try {
        return _values[periodNumber];
    } catch (ArrayIndexOutOfBoundsException e) {
        return 0;
    }
}

通過這項重構後:

double getValueForPeriod(int periodNumber) {
    if (periodNumber >= _values.length) {
        return 0;
    }
    return _values[periodNumber];
}

第11章 處理概括關係

11.1 Pull Up Field (字段上移)

兩個子類擁有相同的字段。

思路:將該字段移至超類。

11.2 Pull Up Method (函數上移)

有些函數,在各個子類中產生完全相同的結果。

思路:將該函數移至超類。

11.3 Pull Up Constructor Body (構造函數本體上移)

你在各個子類中擁有一些構造函數,它們的本體幾乎完全一致。

思路:在超類中新建一個構造函數,並在子類構造函數中調用它。

eg.

class Manager extends Employee...
    public Manager(String name, String id, int grade) {
        _name = name;
        _id = id;
        _grade = grade;
    }

通過這項重構後:

public Manager(String name, String id, int grade) {
    super(name, id);
    _grade = grade;
}

11.4 Push Down Method (函數下移)

超類中的某個函數只與部分(而非全部)子類有關。

思路:將這個函數移到相關的那些子類去。

11.5 Push Down Field (字段下移)

超類中的某個字段只被部分(而非全部)子類用到。

思路:將這個字段移到需要它的那些子類去。

11.6 Extract Subclass (提煉子類)

類中的某些特性只被某些 (而非全部)實例用到。

思路:新建一個子類,將上面所說的那一部分特性移到子類中。

11.7 Extract Superclass (提煉超類)

兩個類有相似特性。

思路:爲這兩個類建立一個超類,將相同特性移至超類。

11.8 Extract Interface (提煉接口)

若干客戶使用類接口中的同一子集,或者兩個類的接口有部分相同。

思路:將相同的子集提煉到一個獨立接口中。

11.9 Collapse Hierarchy (摺疊繼承體系)

超類和子類之間無太大區別。

思路:將它們合爲一體。

11.10 Form Template Method (塑造模板函數)

你有一些子類,其中相應的某些函數以相同順序執行類似的操作,但各個操作的細節上有所不同。

思路:將這些操作分別放進獨立函數中,並保持它們都有相同的簽名,於是原函數也就變得相同了。然後將原函數上移至超類。

11.11 Replace Inheritance with Delegation (以委託取代繼承)

某個子類只使用超類接口中的一部分,或是根本不需要繼承而來的數據。

思路:在子類中新建一個字段用以保存超類;調整子類函數,令它改而委託超類;然後去掉兩者之間的繼承關係。

11.12 Replace Delegation with Inheritance (以繼承取代委託)

你在兩個類之間使用委託關係,並經常爲整個接口編寫許多極簡單的委託函數。

思路:讓委託類繼承受託類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章