2012年2月7日 星期二

《重構:改善既有程式的設計》筆記

一. 重構原則

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


2.你不該為重構而重構,你之所以重構,是因為你想做別的什麼事,而重構可以幫助你把那些事做好


3.重構的時機:添加新功能時、除錯時、複審程式碼時


4.你的複審團隊最好是一個複審者搭配一個原作者。由複審者提出修改建議,然後兩個人共同判斷這些修改是否能夠透過重構輕鬆實現。若能如此,就兩人一起著手修改

若是比較大的設計複審工作,那麼,在一個較大團隊內保留多種觀點會比較好。此時應運用 UML 展現設計,並以 CRC 卡展示軟體情節,而不要直接展示程式碼


5.當你感覺需要撰寫註釋,應先嘗試重構,試著讓所有註釋都變得多餘




二. 構築測試體系

1.每個 class 都應該有一個測試函式,並以它來測試自己這個 class


2.確保所有測試都完全自動化,讓它們檢查自己的測試結果


3.撰寫測試碼最有用的時機是在開始編程之前。當你需要添加特性的時候,就應先寫相應的測試碼


4.把注意力集中在介面而非實作上


5.要能輕鬆執行多個測試,你可以建立一個獨立的 class 用於測試,並在一個框架(如 JUnit)中執行它


6.一開始撰寫測試碼時,應先讓它們失敗(修改既有的程式碼),你才能知道測試機制是否可以正確執行,並且的確測試了它該測試的東西


7.每當你接獲臭蟲提報時,先撰寫一個單元測試來揭發這隻臭蟲


8.當你要添加更多的測試時,你應先觀察 class 該做的所有事情,然後針對任何一項功能的任何一種可能失敗的情況,進行測試


9.測試的要訣是:測試你最擔心出錯的部分。花合理的時間抓出大多數臭蟲,好過窮盡一生抓出所有臭蟲


10.考慮可能出錯的邊界條件,並把測試火力集中在那


11.當事情被大家認為應該會出錯時,別忘了檢查同時是否有異常如預期般地被拋出




三. 重新組織你的函式

1.《Extract Method》(提煉函式)

情境:你有一段程式碼可以被組織在一起並獨立出來

解決方式:將這段程式碼放進一個獨立函式中,並讓函式名稱解釋該函式的用途

作法:
(1)創造一個新函式,根據這個函式的意圖來給它命名 (以它「做什麼」來命名,而不是以它「怎麼做」來命名)

(2)將提煉出的程式碼從源函式 (source) 拷貝到新建的目標函式中

(3)仔細檢查提煉出的程式碼,看看其中是否引用了「作用域 (scope) 限於源函式」的變數 (包括區域變數和源函式參數)

(4)檢查是否有「僅用於被提煉碼」的暫時變數 (temporary variables)。如果有,就在目標函式中將它們宣告為暫時變數

(5)檢查被提煉碼,看看是否有任何區域變數的值被它改變。如果一個暫時變數的值被修改了,看看是否可以將被提煉碼處理為一個查詢 (query),並將結果賦值給相關變數

*如果很難這樣做,或如果被修改的變數不只一個,你就不能僅僅將這段程式碼原封不動地提煉出來。你可能需要先使用《Split Temporary Variable》,然後再嘗試提煉。也可以使用《Replace Temp with Query》將暫時變數消滅掉

(6)將提煉碼中需要讀取的區域變數,當作參數傳給目標函式

(7)處理完所有區域變數之後,進行編譯

(8)在源函式中,將被提煉碼替換為「對目標函式的呼叫」

*如果你將任何暫時變數移到目標函式中,請檢查它們原本的宣告式是否在被提煉碼的外圍。如果是,現在就可以刪除這些宣告式了

(9)編譯,測試


2.《Inline Method》(將函式內聯化)

情境:在一個函式中,其本體應該與其名稱同樣清楚易懂

解決方式:在函式呼叫點插入函式本體,然後移除該函式

作法:
(1)檢查函式,確定它不具多樣性(polymorphic)

*如果 subclass 繼承了這個函式,就不要將此函式 inline 化,因為 subclass 無法覆寫一個根本不存在的函式

(2)找出這個函式的所有被呼叫點

(3)將這個函式的所有被呼叫點都替換為函式本體(程式碼)

(4)編譯,測試

(5)刪除該函式的定義


3.《Inline Temp》(將暫時變數內聯化)

情境:你有一個暫時變數,只被一個簡單運算式賦值一次,而它妨礙了其他重構手法

解決方式:將所有對該變數的引用動作,替換為對它賦值的那個運算式本身

作法:
(1)如果這個暫時變數並未被宣告為 final,那就將它宣告為 final,然後編譯

*這個動作可以檢查該暫時變數是否真的只被賦值一次

(2)找到該暫時變數的所有引用點,將它們替換為「為暫時變數賦值」之述句中的等號右側運算式

(3)每次修改後,編譯並測試

(4)修改完所有引用點之後,刪除該暫時變數的宣告式和賦值述句

(5)編譯,測試


4.《Replace Temp With Query》(以查詢取代暫時變數)

情境:你的程式以一個暫時變數來保存某一運算式的運算結果

解決方式:將這個運算式提煉到一個獨立函式中。將這個暫時變數的所有「被引用點」替換為「對新函式的呼叫」。新函式可被其他函式使用

作法:
(1)找出只被賦值一次的暫時變數

*如果某個暫時變數被賦值超過一次,則考慮使用《Split Temporary Variable》將它分割成多個變數

(2)將該暫時變數宣告為 final

(3)編譯

* 這個動作可確保該暫時變數的確只被賦值一次

(4)將「對該暫時變數賦值」之述句的等號右側部分提煉到一個獨立函式中

*1 首先將函式宣告為 private。日後你可能會發現有更多 class 需要使用它,那時你可輕易放鬆對它的保護

*2 確保提煉出來的函式無任何連帶影響(副作用),也就是說該函式並不修改任何物件內容。如果它有連帶影響,就對它進行《Separate Query form Modifier》

(5)編譯,測試

(6)在該暫時變數身上實施《Inline Temp》


5.《Introduce Explaining Variable》(引入解釋性變數)

情境:你有一個複雜的運算式

解決方式:將該複雜運算式(或其中一部分)的結果放進一個暫時變數,以此變數名稱來解釋運算式的用途

作法:
(1)宣告一個 final 暫時變數,將待分解之複雜運算式中的一部分動作的運算結果賦值給它

(2)將運算式中的「運算結果」這一部分,替換為上述暫時變數

*如果被替換的這一部分在程式碼中重複出現,你可以每次一個,逐一替換

(3)編譯,測試

(4)重複上述過程,處理運算式的其他部分


6.《Split Temporary Variable》(剖解暫時變數)

情境:你的程式有某個暫時變數被賦值超過一次,它既不是迴圈變數,也不是一個集用暫時變數(collecting temporary variable)

解決方式:針對每次賦值,創造一個獨立的、對應的暫時變數

作法:
(1)在「待剖解」之暫時變數的宣告式及其第一次被賦值處,修改其名稱

*如果稍後之賦值述句是「i = i + 某運算式」這種形式,就意味這是個集用暫時變數,那麼就不要剖解它。集用暫時變數的作用通常是累加、字串接合、寫入 stream、或者向群集添加元素

(2)將新的暫時變數宣告為 final

(3)以該暫時變數之第二次賦值動作為界,修改此前對該暫時變數的所有引用點,讓它們引用新的暫時變數

(4)在第二次賦值處,重新宣告圓心那個暫時變數

(5)編譯,測試

(6)逐次重複上述過程。每次都在宣告處將暫時變數改名,並修改下次賦值之前的引用點


7.《Remove Assignments to Parameters》(移除對參數的賦值動作)

情境:你的程式碼對一個參數進行賦值動作

解決方式:以一個暫時變數取代該參數的位置

作法:
(1)建立一個暫時變數,把待處理的參數值賦予它

(2)以「對參數的賦值動作」為界,將其後所有對此參數的引用點,全部替換為「對此暫時變數的引用動作」

(3)修改賦值述句,使其改為對新建之暫時變數賦值

(4)編譯,測試

*如果程式碼的語意是 pass by reference,請在呼叫端檢查呼叫後是否還使用了這個參數。也要檢查有多少個 pass by reference 參數「被賦值後又被使用」。

盡量只以 return 方式傳回一個值。如果需要回返的值不只一個,看看可否把需回返的大堆資料變成單一物件,或乾脆為每個回返值設計對應的一個獨立函式


8.《Replace Method with Method Object》(以函式物件取代函式)

情境:你有一個大型函式,其中對區域變數的使用,使你無法採行《Extract Method》

解決方式:將這個函式放進一個單獨物件中,如此一來區域變數就成了物件內的欄位。然後你可以在同一個物件中將這個大型函式分解為數個小型函式

作法:
(1)建立一個新 class,根據「待被處理之函式」的用途,為這個 class 命名

(2)在新 class 中建立一個 final 欄位,用以保存原先大型函式所駐物件(源物件)。同時,針對原函式的每個暫時變數和每個參數,在新 class 中建立一個對應的欄位保存之

(3)在新 class 中建立一個建構式,接收源物件及原函式的所有參數作為參數

(4)在新 class 中建立一個 compute() 函式

(5)將原函式的程式碼拷貝到 compute() 函式中。如果需要呼叫源物件的任何函式,請以「源物件」欄位喚起

(6)編譯

(7)將舊函式的函式本體替換為這樣一條述句:「創建上述新 class 的一個新物件,而後呼叫其中的 compute() 函式」


9.《Substitute Algorithm》(替換你的演算法)

情境:你想要把某個演算法替換為另一個更清晰的演算法

解決方式:將函式本體替換為另一個演算法

作法:
(1)準備好替換用的演算法,讓它通過編譯

(2)針對現有測試,執行上述的新演算法。如果結果與原本結果相同,重構便結束

(3)如果測試結果不同於原先,在測試和除錯過程中,以舊演算法為此比較參考標準

*對於每個測試案例,分別以新舊兩種演算法執行,並觀察兩者結果是否相同。這可以幫助你看到哪一個測試案例出現麻煩,以及出現了怎樣的麻煩




四. 在物件之間移動特性

1.《Move Method》(搬移函式)

情境:你的程式中,有個函式與其所駐 class 之外的另一個 class 進行更多交流: 呼叫後者,或被後者呼叫

解決方式:在該函式最常引用(指涉)的 class 中建立一個有著類似行為的新函式。將舊函式變成一個單純的委託函式(delegating method),或是將舊函式完全移除

作法:
(1)檢查 source class 定義之 source method 所使用的一切特性(指 class 定義的所有東西,包括欄位和函式),考慮它們是否也該被搬移

*如果某個特性只被你打算搬移的那個函式用到,你應該將它一併搬移。如果另有其他函式使用了這個特性,你可以考慮將使用該特性的所有函式全都一併搬移。有時候搬移一組函式比逐一搬移簡單些

(2)檢查 source class 的 subclass 和 superclass,看看是否有該函式的其他宣告

*如果出現其他宣告,你或許無法進行搬移,除非 target class 也同樣表現出多型性(polymorphism)

(3)在 target class 中宣告這個函式

*你可以為此函式選擇一個新名稱–對 target class 更有意義的名稱

(4)將 source method 的程式碼拷貝到 target method 中。調整後者,使其能在新家中正常執行

*1 如果 target method 使用了 source 特性,你決定如何從 target method 引用 source object。如果 target class 中沒有相應的引用機制,就把 source object reference 當作參數,傳給新建立的 target method

*2 如果 source method 包含異常處理式(exception handler),你得判斷邏輯上應該由哪個 class 來處理這一異常。如果應該由 source class 來負責,就把異常處理式留在原地

(5)編譯 target class

(6)決定如何從 source 正確引用 target object

*可能會有一個現成的欄位或函式幫助你取得 target object。如果沒有,就看能否輕鬆建立一個這樣的函式。如果還是不行,你得在 source class 中新建一個新欄位來保存 target object。

這可能是一個永久性修改,但你也可以讓它保持暫時的地位,因為後繼的其他重構項目可能會把這個新建欄位去掉

(7)修改 source method,使之成為一個 delegating method(純委託函式)

(8)編譯,測試

(9)決定「刪除 source method」或將它當作一個 delegating method 保留下來

*如果你經常要在 source object 中引用 target method,那麼將 source method 作為 delegating method 保留下來會比較簡單

(10)如果你移除了 source method,請將 source class 中對 source method 的所有引用動作,替換為「對 target method 的引用動作」

*你可以每修改一個引用點就編譯並測試一次。也可以藉由一次「搜尋/替換」改掉所有引用點,這通常簡單一些

(11)編譯,測試


2.《Move Field》(搬移欄位)

情境:你的程式中,某個 field(欄位) 被其所駐 class 之外的另一個 class 更多地用到

解決方式:在 target class 建立一個 new field,修改 source field 的所有用戶,令它們改用 new field

作法:
(1)如果 field 的屬性是 public,首先使用《Encapsulate Field》將它封裝起來

*如果你有可能移動那些頻繁存取該 field 的函式,或如果有許多函式存取某個 field,就先使用《Self Encapsulate Field》也許會有幫助

(2)編譯,測試

(3)在 target class 中建立與 source field 相同的 field,並同時建立相應的設值/取值(setting/getting)函式

(4)編譯 target class

(5)決定如何在 source object 中引用 target object

*一個現成的 field 或 method 可以幫助你得到 target object。如果沒有,就看能否輕易建立這樣一個函式。如果還不行,就得在 source class 中新建一個 field 來存放 target object。

這可能是個永久性修改,但你也可以暫不公開它,因為後續重構可能會把這個新建 field 除掉

(6)刪除 source field

(7)將所有「對 source field的引用」替換為「對 target 適當函式的呼叫」

*1 如果是「讀取」該變數,就把「對 source field 的引用」替換為「對 target 取值函式(getter)的呼叫」

*2 如果 source field 不是 priveat,就必須在 source class 的所有 subclasses 中搜尋 source field 的引用點,並進行相應替換

(8)編譯,測試


3.《Extract Class》(提煉類別)

情境:某個 class 做了應該由兩個 classes 做的事

解決方式:建立一個新 class,將相關的欄位和函式從舊 class 搬移到新 class

作法:
(1)決定如何分解 class 所負的責任

(2)建立一個新 class,用以表現從舊 class 中分離出來的責任

*如果舊 class 剩下的責任與舊 class 名稱不符,便為舊 class 改名

(3)建立「從舊 class 存取新 class」的連接關係

*也許你有可能需要一個雙向連接。但是在真正需要它之前,不要建立「從新 class 通往舊 class」的連接

(4)對於你想搬移的每一個欄位,運用《Move Field》搬移之

(5)每次搬移後,編譯、測試

(6)使用《Move Method》將必要函式搬移到新 class。先搬移較低階函式(也就是「被其他函式呼叫」多餘「呼叫其他函式」者),再搬移較高階函式

(7)每次搬移後,編譯、測試

(8)檢查,精簡每個 class 的介面

*如果你建立起雙向連接,檢查是否可以將它改為單向連接

(9)決定是否讓新 class 曝光。如果你的確需要曝光它,就決定讓它成為 reference object(引用型物件) 或 immutable value object(不可變之「實質型物件」)


4.《Inline Class》(將類別內聯化)

情境:你的某個 class 沒有做太多事情(沒有承擔足夠責任)

解決方式:將 class 的所有特性搬移到另一個 class 中,然後移除原 class

作法:
(1)在 absorbing class(合併端的那個 class) 身上宣告 source class 的 public 協定,並將其中所有函式委託(delegate)至 source class

*如果「一個獨立介面表示 source class 函式」更合適的話,就應該在 inlining 之前先使用《Extract Interface》

(2)修改所有 source class 引用點,改而引用 absorbing class

*將 source class 宣告為 private,以斬斷 package 之外的所有引用可能。同時並修改 source class 的名稱,這便可使編譯器幫助你捕捉到所有對於 source class 的 dangling references(虛懸引用點)

(3)編譯,測試

(4)運用《Move Method》和《Move Field》,將 source class 的特性全部搬移到 absorbing class

(5)移除 source class


5.《Hide Delegate》(隱藏「委託關係」)

情境:客戶直接呼叫其 server object 的 delegate class

解決方式:在 server 端建立客戶所需的所有函式,用以隱藏委託關係

作法:
(1)對於每一個委託關係中的函式,在 server 端建立一個簡單的委託函式

(2)調整客戶,令它只呼叫 server 提供的函式(不得跳過逕自呼叫下層)

*如果 client 和 server 不在同一個 package,考慮修改委託函式的存與權限,讓 client 得以在 package 之外呼叫它

(3)每次調整後,編譯並測試

(4)如果將來不再有任何客戶需要取用受託類別,便可移除 server 中的相關存取函式

(5)編譯,測試


6.《Remove Middle Man》(移除中間人)

情境:某個 class 做了過多的簡單委託動作(simple delegation)

解決方式:讓客戶直接呼叫 delegate(受託物件)

作法:
(1)建立一個函式,用以取用 delegate

(2)對於每個委託函式,在 server 中刪除該函式,並將「客戶對該函式的呼叫」替換為「對 delegate 的呼叫」

(3)處理每個委託函式後,編譯、測試


7.《Introduce Foreign Method》(引入外加函式)

情境:你所使用的 server class 需要"一個"額外函式,但你無法修改這個 class

解決方式:在 client class 中建立一個函式,並以一個 server class 實體作為第一引數(argument)

作法:
(1)在 client class 中建立一個函式,用來提供你需要的功能

*這個函式不應該取用 client class 的任何特性。如果它需要一個值,就把該值當作參數傳給它

(2)以 server class 實體作為該函式的第一個參數

(3)將該函式註釋為:「外加函式(foreign method),應在 server class 實現」

*這麼一來,將來如果有機會將外加函式搬移到 server class 中,你便可以輕鬆找出這些外加函式


8.《Introduce Local Extension》(引入區域性擴展)

情境:你所使用的 server class 需要"一些"額外函式,但你無法修改這個 class

解決方式:建立一個新 class,使它包含這些額外函式。讓這個擴展品成為 source class 的 subclass 或 wrapper(外覆類別)

作法:
(1)建立一個 extension class,將它作為原類別的 subclass 或 wrapper

(2)在 extension class 中加入轉型建構式(converting constructors)

*所謂「轉型建構式」是指接受原物(original)作為參數。如果你採用 subclassing 方案,那麼轉型建構式應該呼叫適當的 superclass 建構式;如果你採用 wrapper 方案,那麼轉型建構式應該將它所獲得之引數賦值給「用以保存委託關係」的那個欄位

(3)在 extension class 中加入新特性

(4)根據需要,將原類別替換為擴展類別(extension)

(5)將「針對原始類別而定義的所有外加函式」搬移到擴展類別中




五. 重新組織你的資料

1.《Self Encapsulate Field》(自我封裝欄位)

情境:你直接存取一個欄位,但與欄位之間的耦合關係逐漸變得笨拙

解決方式:為這個欄位建立取值/設值函式,並且只以這些函式來存取欄位

作法:
(1)為「待封裝欄位」建立取值/設值函式

(2)找出該欄位的所有引用點,將它們全部替換為「對於取值/設值函式的叫用」

*1 如果引用點是「讀取」欄位值,就將它替換為「呼叫取值函式」;如果引用點是「設定」欄位值,就將它替換為「呼叫設值函式」

*2 你可以暫時為該欄位改名,讓編譯器幫助你搜尋引用點

(3)將該欄位宣告為 private

(4)複查,確保找出所有引用點

(5)編譯,測試


2.《Replace Date Value with Object》(以物件取代資料值)

情境:你有一筆資料項(data item),需要額外的資料和行為

解決方式:將這筆資料項變成一個物件

作法:
(1)為「待替換數值」新建一個 class,在其中宣告一個 final 欄位,其型別和 source class 中的「待替換數值」型別一樣。然後在新 class 中加入這個欄位的取值函式,再加上一個「接受此欄位為參數」的建構式

(2)編譯

(3)將 source class 中的「待替換數值欄位」的型別改為上述的新建 class

(4)修改 source class 中此一欄位的取值函式,令它呼叫新建 class 的取值函式

(5)如果 source class 建構式中提及這個「待替換欄位」(多半是賦值動作),就修改建構式,令它改用新 class 的建構式來對欄位進行賦值動作

(6)修改 source class 中「待替換欄位」的設值函式,令它為新 class 創建一個實體

(7)編譯,測試

(8)現在,你有可能需要對新 class 使用《Change Value to Reference》


3.《Change Value to Reference》(將實值物件改為引用物件)

情境:你有一個 class,衍生出許多相等實體(equal instances),你希望將它們替換為單一物件

解決方式:將這個 value object(實質物件) 變成一個 reference object(引用物件)

作法:
(1)使用《Replace Constructor with Factory Method》

(2)編譯,測試

(3)決定由什麼物件負責提供存取新物件的途徑

*1 可能是個靜態字典(static dictionary)或一個註冊物件(registry object)

*2 你也可以使用多個物件作為新物件的存取點(access point)

(4)決定這些 reference object 應該預先創建好,或是應該動態創建

*如果這些 reference object 是預先創建好的,而你必須從記憶體將它們讀取出來,那麼就得確保它們在被需要的時候能夠被及時載入

(5)修改 factory method(此指「用以創建某種實體」的函式),令它傳回 reference object
*1 如果物件是預先創建好的,你就需要考慮: 萬一有人索求一個其實並不存在的物件,要如何處理錯誤?

*2 你可能希望對 factory method 使用《Rename Method》,使其傳達這樣的資訊: 它傳回的是一個既存物件

(6)編譯,測試


4.《Change Reference to Value》(將引用物件改為實值物件)

情境:你有一個 reference object,很小且不可變(immutable),而且不易管理

解決方式:將它變成一個 value object

作法:
(1)檢查重構對象是否為 immutable 物件,或是否可修改為 immutable 物件

*1 如果該物件目前還不是 immutable,就使用《Remove Setting Method》,直到它成為 immutable 為止

*2 如果無法將物件修改為 immutable,就放棄使用本項重構

(2)建立 equals() 和 hashCode()

(3)編譯,測試

(4)考慮是否可以刪除 factory method(此指「用以創建某種實體」的函式),並將建構式宣告為 public


5.《Replace Array with Object》(以物件取代陣列)

情境:你有一個陣列,其中的元素各自代表不同的東西

解決方式:以物件替換陣列。對於陣列中的每個元素,以一個欄位表示之

作法:
(1)新建一個 class 表示陣列所示資訊,並在該 class 中以一個 public 欄位保存原先的陣列

(2)修改陣列的所有用戶,讓它們改用新建的 class 實體

(3)編譯,測試

(4)逐一為陣列元素添加取值/設值函式。根據元素的用途,為這些存取函式命名。修改用戶端程式碼,讓它們透過存取函式取用陣列內的元素。每次修改後,編譯並測試

(5)當所有「對陣列的直接存取」都被取代為「對存取函式的呼叫」後,將 class 之中保存該陣列的欄位宣告為 private

(6)編譯

(7)對於陣列內的每一個元素,在新 class 中創建一個型別相當的欄位;修改該元素的存取函式,令它改用上述的新建欄位

(8)每修改一個元素,編譯並測試

(9)陣列的所有元素都在對應的 class 內有了相應欄位之後,刪除該陣列


6.《Duplicate Observed Data》(複製「被監視資料」)

情境:你有一些 domain data 置身於 GUI 控件中,而 domain method 需要存取之

解決方式:將該筆資料拷貝到一個 domain object 中。建立一個 Observer 範式,用以對 domain object 和 GUI object 內的重複資料進行同步控制(sync)

作法:
(1)修改 presentation class,使其成為 domain class 的 Observer

*1 如果尚未有 domain class,就建立一個

*2 如果沒有「從 presentation class 到 domain class」的關聯性,就將 domain class 保存於 presentation class 的一個欄位中

(2)針對 GUI class 內的 domain data,使用《Self Encapsulate Field》

(3)編譯,測試

(4)在事件處理函式(event handler)中加上對設值函式的呼叫,以「直接存取方式」(亦即直接呼叫組件提供的相關函式)更新GUI 組件

*1 在事件處理函式中放一個設值函式,利用它將 GUI 組件更新為 domain data 的當前值。如此使用 setter,表示允許其中的任何動作得以於日後被執行起來

*2 進行這個改變時,對於組件,不要使用取值函式,應該採取「直接取用」方式(亦即直接呼叫 GUI 組件所提供的函式)

*3 確保你的測試碼能夠觸發新添加的事件處理機制

(5)編譯,測試

(6)在 domain class 中定義資料及其相關存取函式(accessors)

*1 確保 domain class 中的設值函式能夠觸發 Observer 範式的通報機制(notify mechanism)

*2 對於被觀察(被監視)的資料,在 domain class 中使用「與 presentation class 所用的相同型別」(通常是字串)來保存。後續重構中你可以自由改變這個資料型別

(7)修改 presentation class 中的存取函式,將它們的操作對象改為 domain object(而非 GUI 組件)

(8)修改 observer(亦即 presentation class)的 update(),使其從相應的 domain object 中將所需資料拷貝給 GUI 組件

(9)編譯,測試


7.《Change Unidirectional Association to Bidirectional》(將單向關聯改為雙向)

情境:兩個 classes 都需要使用對方特性,但其間只有一條單向連結

解決方式:添加一個反向指標,並使修改函式(modifiers)能夠同時更新兩條連結(這裡的指標等同於控制碼(handle);修改函式指的是改變雙方關係者)

作法:
(1)在 referred class 中增加一個欄位,用以保存「反向指標」

(2)決定由哪個 class(引用端或被引用端) 控制關聯性

(3)在「被控端」建立一個輔助函式,其命名應該清楚指出它的有限用途

(4)如果既有的修改函式在「控制端」,就讓它負責更新反向指標

(5)如果既有的修改函式在「被控端」,則在「控制端」建立一個控制函式,並讓既有的修改函式呼叫這個新建的控制函式


8.《Change Bidirectional Association to Unidirectional》(將雙向關聯改為單向)

情境:兩個 classes 之間有雙向關聯,但其中一個 class 已經不再需要另一個 class 的特性

解決方式:去除不必要的關聯

作法:
(1)找出「你想去除的指標」的保存欄位,檢查它的每一個使用者,判斷是否可以去除該指標

*1 不但要檢查「直接讀取點」,也要檢查「直接讀取點」的呼叫函式

*2 考慮有無可能不通過指標取得「被引用物件」。如果有可能,你就可以對取值函式使用《Substitute Algorithm》,從而讓客戶在沒有指標的情況下也可以使用該取值函式

*3 對於使用該欄位的所有函式,考慮將「被引用物件」作為引數(argument)傳進去

(2)如果客戶使用了取值函式,先運用《Self Encapsulate Field》將「待除欄位」自我封裝起來,然後使用《Substitute Algorithm》對付取值函式,令它不再使用該(待除)欄位。然後編譯、測試

(3)如果客戶並未使用取值函式,那就直接修改「待除欄位」的所有被引用點: 改以其他途徑獲得該欄位所保存的物件。每次修改後,編譯並測試

(4)如果已經沒有任何函式使用該(待除)欄位,就移除所有「對該欄位的更新邏輯」,然後移除該欄位

*如果有許多地方對此欄位賦值,先運用《Self Encapsulate Field》使這些地點改用同一個設值函式。編譯、測試。而後將這個設值函式的本體清空。再編譯、測試。

如果這些都可行,就可以將此欄位和其設值函式,連同對設值函式的所有呼叫,全部移除

(5)編譯,測試


9.《Replace Magic Number with Symbolic Constant》(以符號常數(字面常數)取代魔術數字)

情境:你有一個字面數值(literal number),帶有特別含義

解決方式:創造一個常數,根據其意義為它命名,並將上述的字面數值替換為這個常數

作法:
(1)宣告一個常數,令其值為原本的魔術數字值

(2)找出這個魔術數字的所有引用點

(3)檢查是否可以使用這個新宣告的常數來替換該魔術數字。如果可以,便以此一常數替換之

(4)編譯

(5)所有魔術數字都被替換完畢後,編譯並測試。此時整個程式應該運轉如常,就像沒有做任何修改一樣

*有個不錯的測試辦法: 檢查現在的程式是否可以被你輕鬆地修改常數值(這可能意味某些預期結果將有所改變,以配合這一新值。實際工作中並非總是可以進行這樣的測試)。如果可行,這就是一個不錯的手法


10.《Encapsulate Field》(封裝欄位)

情境:你的 class 中存在一個 public 欄位

解決方式:將它宣告為 private,並提供相應的存取函式

作法:
(1)為 public 欄位提供取值/設值函式

(2)找到這個 class 以外使用該欄位的所有地點。如果客戶只是「使用」該欄位,就把引用動作替換為「對取值函式的呼叫」;如果客戶修改了該欄位值,就將此一引用點替換為「對設值函式的呼叫」

*如果這個欄位是個物件,而客戶只不過是呼叫該物件的某個函式,那麼無論該函式是否為修改函式,都只能算是「使用」該欄位。只有當客戶為該欄位賦值時,才能將其替換為設值函式

(3)每次修改之後,編譯並測試

(4)將欄位的所有客戶修改完畢後,把欄位宣告為 private

(5)編譯,測試


11.《Encapsulate Collection》(封裝群集)

情境:有個函式傳回一個群集

解決方式:讓這個函式傳回該群集的一個唯讀映件(real-only view),並在這個 class 中提供「添加/移除」群集元素的函式

作法:
(1)加入「為群集添加、移除元素」的函式

(2)將「用以保存群集」的欄位初始化為一個空群集

(3)編譯

(4)找出「群集設值函式」的所有呼叫者。你可以修改那個設值函式,讓它使用上述新建立的「添加/移除元素」函式;也可以直接修改呼叫端,改讓它們呼叫上述新建立的「添加/移除元素」函式

*1 兩種情況下需要用到「群集設值函式」: (1)群集為空時 (2)準備將原有群集替換為另一個群集時

*2 你或許會想運用《Rename Method》為「群集設值函式」改名,從 setXxx() 改為 initializeXxx() 或 replaceXxx()

(5)編譯,測試

(6)找出所有「透過取值函式獲得群集並修改其內容」的函式。逐一修改這些函式,讓它們改用「添加/移除」函式。每次修改後,編譯並測試

(7)修改完上述所有「透過取值函式獲得群集並修改其內容」的函式後,修改取值函式本身,使它傳回該群集的一個唯讀映件

*你可以使用 Collection.unmodifiableXxx() 得到該群集的唯讀映件

(8)編譯,測試

(9)找出取值函式的所有使用者,從中找出應該存在於「群集之宿主物件(host object)」內的程式碼。運用《Extract Method》和《Move Method》將這些程式碼移到宿主物件去


12.《Replace Record with Data Class》(以資料類別取代記錄)

情境:你需要面對傳統編程環境中的 record structure(記錄結構)

解決方式:為該 record 創建一個「啞」資料物件(dumb data object)

作法:
(1)新建一個 class,表示這個 record

(2)對於 record 中的每一筆資料項,在新建的 class 中建立對應的一個 private 欄位,並提供相應的取值/設值函式


13.《Replace Type Code with Class》(以類別取代型別代碼)

情境:class 之中有一個數值型別代碼(numeric type code),但它並不影響 class 的行為

解決方式:以一個新的 class 替換該數值型別代碼

作法:
(1)為 type code 建立一個 class

*這個 class 內需要一個用以記錄 type code 的欄位,其型別應該和 type code 相同,並應該有對應的取值函式。此外還應該用一組 static 變數保存「允許被創建」的實體,並以一個 static 函式根據原本的 type code 傳回合適的實體

(2)修改 source class 實作碼,讓它使用上述新建的 class

*維持原先以 type code 為基礎的函式介面,但改變 static 欄位,以新建的 class 產生代碼。然後,修改 type code 相關函式,讓它們也從新建的 class 中獲取代碼

(3)編譯,測試

*此時,新建的 class 可以對 type code 進行執行期檢查

(4)對於 source class 中每一個使用 type code 的函式,相應建立一個函式,讓新函式使用新建的 class

*你需要建立「以新 class 實體為引數」的函式,用以替換原先「直接以 type code 為引數」的函式。你還需要建立一個「傳回新 class 實體」的函式,用以替換原先「直接傳回 type code」的函式。

建立新函式前,你可以使用《Rename Method》修改原函式名稱,明確指出那些函式仍然使用舊式的 type code

(5)逐一修改 source class 用戶,讓它們使用新介面

(6)每修改一個用戶,編譯並測試

*你也可能需要一次性修改多個彼此相關的函式,才能保持這些函式之間的一致性,才能順利地編譯、測試

(7)刪除「使用 type code」的舊介面,並刪除「保存舊 type code」的靜態變數

(8)編譯,測試


14.《Replace Type Code with Subclasses》(以子類別取代型別代碼)

情境:你有一個不可變的(immutable) type code,它會影響 class 的行為

解決方式:以一個 subclass 取代這個 type code

作法:
(1)使用《Self Encapsulate Field》將 type code 自我封裝起來

*如果 type code 被傳遞給建構式,你就需要將建構式換成 factory method(此指「用以創建某種實體」的函式)

(2)為 type code 的每一個數值建立一個相應的 subclass。在每個 subclass 覆寫 type code 的取值函式,使其傳回相應的 type code 值

*這個值被編死在 return 句中。當所有的 case 子句都被替換後,問題就解決了

(3)每建立一個新的 subclass,編譯並測試

(4)從 superclass 中刪除保存 type code 的欄位。將 type code 存取函式宣告為抽象函式

(5)編譯,測試


15.《Replace Type Code with State/Strategy》(以 State/Strategy 取代型別代碼)

情境:你有一個 type code,它會影響 class 的行為,但你無法使用 subclassing

解決方式:以 state object(專門用來描述狀態的物件) 取代 type code

作法:
(1)使用《Self Encapsulate Field》將 type code 自我封裝起來

(2)新建一個 class,根據 type code 的用途為它命名。這就是一個 state object

(3)為這個新建的 class 添加 subclasses,每個 subclass 對應一種 type code

*比起逐一添加,一次性加入所有必要的 subclasses 可能更簡單些

(4)在 superclass 中建立一個抽象的查詢函式(abstract query),用以傳回 type code。在每個 subclass 中覆寫該函式,傳回確切的 type code

(5)編譯

(6)在 source class 中建立一個欄位,用以保存新建的 state object

(7)調整 source class 中負責查詢 type code 的函式,將查詢動作轉發給 state object

(8)調整 source class 中「為 type code 設值」的函式,將一個恰當的 state object subclass 賦值給「保存 state object」的那個欄位

(9)編譯,測試


16.《Replace Subclass with Fields》(以欄位取代子類別)

情境:你的各個 subclasses 的唯一差別只在「傳回常數資料」的函式身上

解決方式:修改這些函式,使它們傳回 superclass 中的某個(新增)欄位,然後銷毀 subclasses

作法:
(1)對所有 subclasses 使用《Replace Constructor with Factory Method》

(2)如果有任何程式碼直接引用 subclass,令它改而引用 superclass

(3)針對每個常數函式,在 superclass 中宣告一個 final 欄位

(4)為 superclass 宣告一個 protected 建構式,用以初始化這些新增欄位

(5)新建或修改 subclass 建構式,使它呼叫 superclass 的新增建構式

(6)編譯,測試

(7)在 superclass 中實現所有常數函式,令它們傳回相應欄位值,然後將該函式從 subclass 中刪掉

(8)每刪除一個常數函式,編譯並測試

(9)subclass 中所有的常數函式都被刪除後,使用《Inline Method》將 subclass 建構式內聯(inlining)到 superclass 的 factory method(此指「用以創建某種實體」的函式) 中

(10)編譯,測試

(11)將 subclass 刪掉

(12)編譯,測試

(13)重複「inlining 建構式、刪除 subclass」過程,直到所有 subclass 都被刪除





六. 簡化條件式

1.《Decompose Conditional》(分解條件式)

情境:你有一個複雜的條件(if-then-else)述句

解決方式:從 if、then、else 三個段落中分別提煉出獨立函式

作法:
(1)將 if 段落提煉出來,構成一個獨立函式

(2)將 then 段落和 else 段落都提煉出來,各自構成一個獨立函式


2.《Consolidate Conditional Expression》(合併條件式)

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

解決方式:將這些測試合併為一個條件式,並將這個條件式提煉成為一個獨立函式

作法:
(1)確定這些條件述句都沒有副作用(連帶影響)

*如果條件式有副作用,就不能使用本項重構

(2)使用適當的邏輯運算子,將一系列相關條件式合併為一個

(3)編譯,測試

(4)對合併後的條件式實施《Extract Method》


3.《Consolidate Duplicate Conditional Fragments》(合併重複的條件片段)

情境:在條件式的每個分支上有著相同的一段程式碼

解決方式:將這段重複程式碼搬移到條件式之外

作法:
(1)鑑別出「執行方式不隨條件變化而變化」的程式碼

(2)如果這些共通程式碼位於條件式起始處,就將它移到條件式之前

(3)如果這些共通程式碼位於條件式尾端,就將它移到條件式之後

(4)如果這些共通程式碼位於條件式中段,就需要觀察共通程式碼之前或之後的程式碼是否改變了什麼東西。如果的確有改變,應該首先將共通程式碼向前或向後移動,移至條件式的起始處或尾端,再以前面所說的辦法來處理

(5)如果共通程式碼不只一句條件述句,你應該首先使用《Extract Method》將共通程式碼提煉到一個獨立函式中,再以前面所說的辦法來處理


4.《Remove Control Flag》(移除控制旗標)

情境:在一系列布林運算式中,某個變數帶有「控制旗標」的作用

解決方式:以 break 述句或 return 述句取代控制旗標

作法:
(1)找出「讓你得以跳出這段邏輯」的控制旗標值

(2)找出「將"可跳出條件式之值"賦予"旗標變數"」的那個述句,代以恰當的 break 述句或 continue 述句

(3)每次替換後,編譯並測試


5.《Replace Nested Conditional with Guard Clauses》(以衛述句取代巢狀條件式)

情境:函式中的條件邏輯使人難以看清正常的執行路徑

解決方式:使用衛述句(guard clauses)表現所有特殊情況

作法:
(1)對於每個檢查,放進一個衛述句

*衛述句要不就從函式中 return,要不就拋出一個異常

(2)每次將「條件檢查」替換成「衛述句」後,編譯並測試

*如果所有衛述句都導致相同結果,則使用《Consolidate Conditional Expressions》


6.《Replace Conditional with Polymorphism》(以多型取代條件式)

情境:你手上有個條件式,它根據物件型別的不同而選擇不同的行為

解決方式:將這個條件式的每一個分支放進一個 subclass 內的覆寫函式中,然後將原始函式宣告為抽象函式

作法:
(1)如果要處理的條件式是一個更大函式中的一部分,首先對條件式進行分析,然後使用《Extract Method》將它提煉到一個獨立函式去

(2)如果有必要,使用《Move Method》將條件式放置到繼承結構的頂端

(3)任選一個 subclass,在其中建立一個函式,使之覆寫 superclass 中容納條件式的那個函式。將「與該 subclass 相關的條件式分支」拷貝到新建函式中,並對它進行適當調整

*為了順利進行這一步驟,你可能需要將 superclass 中的某些 private 欄位宣告為 protected

(4)編譯,測試

(5)在 superclass 中刪掉條件式內被拷貝出去的分支

(6)編譯,測試

(7)針對條件式的每個分支,重複上述過程,直到所有分支都被移到 subclass 內的函式為止

(8)將 superclass 之中容納條件式的函式宣告為抽象函式


7.《Introduce Null Object》(引入 Null 物件)

情境:你需要再三檢查「某物是否為 null value

解決方式:將 null value 替換為 null object

作法:
(1)為 source class 建立一個 subclass,使其行為像 source class 的 null 版本。在 source class 和 null class 中都加上 isNull() 函式,前者的 isNull() 應該傳回 false,後者的 isNull() 應該傳回 true

*1 你可以建立一個 nullable 介面,將 isNull() 函式放在其中,讓 source class 實現這個介面

*2 你也可以創建一個 testing 介面,專門用來檢查物件是否為 null

(2)編譯

(3)找出所有「索求 source object 卻獲得一個 null」的地方。修改這些地方,使它們改而獲得一個 null object

(4)找出所有「將 source object 與 null 做比較」的地方。修改這些地方,使它們呼叫 isNull() 函式

*1 你可以每次只處理一個 source object 及其客戶程式,編譯並測試後,再處理另一個 source object

*2 你可以在「不該再出現 null value」的地方放上一些 assertions(斷言),確保 null 的確不再出現

(5)編譯,測試

(6)找出這樣的程式點: 如果物件不是 null,做 A 動作,否則做 B 動作

(7)對於每一個上述地點,在 null class 中覆寫 A 動作,使其行為和 B 動作相同

(8)使用上述的被覆寫動作(A),然後刪除「物件是否等於 null」的條件測試。編譯並測試


8.《Introduce Assertion》(引入斷言)

情境:某一段程式碼需要對程式狀態(state)做出某種假設

解決方式:以 assertion(斷言) 明確表現這種假設

作法:
(1)如果你發現程式碼「假設某個條件始終(必須)為真」,就加入一個 assertion 明確說明這種狀況

*你可以新建一個 Assert class,用於處理各種情況下的 assertions




七. 簡化函式呼叫

1.《Rename Method》(重新命名函式)

情境:函式的名稱未能揭示函式的用途

解決方式:修改函式名稱

作法:
(1)檢查函式署名式(signature)是否被 superclass 或 subclass 實作過。如果是,則需要針對每份實作品分別進行下列步驟

(2)宣告一個新函式,將它命名為你想要的新名稱。將舊函式的程式碼拷貝到新函式中,並進行適當調整

(3)編譯

(4)修改舊函式,令它將呼叫轉發給新函式

*編譯,測試

(5)找出舊函式的所有被引用點,修改它們,令它們改而引用新函式。每次修改後,編譯並測試

(6)刪除舊函式

*如果舊函式是 class public 介面的一部分,你可能無法安全地刪除它。這種情況下,應將它保留在原處,並將它標記為 "deprecated"(不再被贊同)

(7)編譯,測試


2.《Add Parameter》(添加參數)

情境:某個函式需要從呼叫端得到更多資訊

解決方式:為此函式添加一個物件參數,讓該物件帶進函式所需資訊

作法:
(1)檢查函式署名式(signature)是否被 superclass 或 subclass 實作過。如果是,則需要針對每份實作品分別進行下列步驟

(2)宣告一個新函式,名稱與原函式相同,只是加上新添的參數。將舊函式的程式碼拷貝到新函式中

*如果需要添加的參數不只一個,將它們一次性添加進去比較容易

(3)編譯

(4)修改舊函式,令它呼叫新函式

*1 如果只有少數幾個地方引用舊函式,你大可跳過這一步驟

*2 此時,你可以給參數提供任意值。但一般來說,我們會給物件參數提供 null,給內建型參數提供一個明顯非正常值。對於數值型參數,應使用 0 以外的值,這樣將來比較容易認出它

(5)編譯,測試

(6)找出舊函式的所有被引用點,將它們全部修改為對新函式的引用。每次修改後,編譯並測試

(7)刪除舊函式

*如果舊函式是 class public 介面的一部分,你可能無法安全地刪除它。這種情況下,應將它保留在原處,並將它標記為 "deprecated"(不再被贊同)

(8)編譯,測試


3.《Remove Parameter》(移除參數)

情境:函式本體不再需要某個參數

解決方式:將該參數去除

作法:
(1)檢查函式署名式(signature)是否被 superclass 或 subclass 實作過。如果是,則需要針對每份實作品分別進行下列步驟

(2)宣告一個新函式,名稱與原函式相同,只是加上新添的參數。將舊函式的程式碼拷貝到新函式中

*如果需要添加的參數不只一個,將它們一次性添加進去比較容易

(3)編譯

(4)修改舊函式,令它呼叫新函式

*如果只有少數幾個地方引用舊函式,你大可跳過這一步驟

(5)編譯

(6)找出舊函式的所有被引用點,將它們全部修改為對新函式的引用。每次修改後,編譯並測試

(7)刪除舊函式

*如果舊函式是 class public 介面的一部分,你可能無法安全地刪除它。這種情況下,應將它保留在原處,並將它標記為 "deprecated"(不再被贊同)

(8)編譯,測試


4.《Separate Query from Modifier》(將查詢函式和修改函式分離)

情境:某個函式既傳回物件狀態值,又修改物件狀態

解決方式:建立兩個不同的函式,其中一個負責查詢,另一個負責修改

作法:
(1)新建一個查詢函式,令它傳回的值與原函式相同

*觀察原函式,看它傳回什麼東西。如果傳回的是一個暫時變數,就找出暫時變數的位置

(2)修改原函式,令它呼叫查詢函式,並傳回獲得的結果

*1 原函式中的每個 return 句都應該像這樣: return new Query(),而不應該傳回其他東西

*2 如果呼叫者將回返值賦給了一個暫時變數,你應該能夠去除這個暫時變數

(3)編譯,測試

(4)將「原函式的每一個被呼叫點」替換為「對查詢函式的呼叫」。然後,在呼叫查詢函式那一行之前,加上對原函式的呼叫。每次修改後,編譯並測試

(5)將原函式的回返值改為 void,並刪掉其中所有的 return 句


5.《Parameterize Method》(令函式攜帶參數)

情境:若干函式做了類似的工作,但在函式本體中卻包含了不同的值

解決方式:建立單一函式,以參數表達那些不同的值

作法:
(1)新建一個帶有參數的函式,使它可以替換先前所有的重複性函式(repetitive methods)

(2)編譯

(3)將「對舊函式的呼叫動作」替換為「對新函式的呼叫動作」

(4)編譯,測試

(5)對所有舊函式重複上述步驟,每次替換後,修改並測試


6.《Replace Parameter with Explicit Method》(以明確函式取代參數)

情境:你有一個函式,其內完全取決於參數值而採取不同反應

解決方式:針對該參數的每一個可能值,建立一個獨立函式

作法:
(1)針對參數的每一種可能值,新建一個明確函式

(2)修改條件式的每個分支,使其呼叫合適的新函式

(3)修改每個分支後,編譯並測試

(4)修改原函式的每一個被呼叫點,改而呼叫上述的某個合適的新函式

(5)編譯,測試

(6)所有呼叫端都修改完畢後,刪除原(帶有條件判斷的)函式


7.《Preserve Whole Object》(保持物件完整)

情境:你從某個物件中取出若干值,將它們作為某一次函式呼叫時的參數

解決方式:改使用(傳遞)整個物件

作法:
(1)對你的目標函式新添一個參數項,用以代表原資料所在的完整物件

(2)編譯,測試

(3)判斷哪些參數可被包含在新添的完整物件中

(4)選擇上述參數之一,將「被呼叫函式」內對該參數的各個引用,替換為「對新添之參數物件的相應取值函式」的呼叫

(5)刪除該項參數

(6)編譯,測試

(7)針對所有「可從完整物件中獲得」的參數,重複上述過程

(8)刪除呼叫端中那些帶有「被刪除之參數」的所有程式碼

*當然,如果呼叫端還在其他地方使用了這些參數,就不要刪除它們

(9)編譯,測試


8.《Replace Parameter with Method》(以函式取代參數)

情境:物件喚起某個函式,並將所得結果作為參數,傳遞給另一個函式。而接受該參數的函式也可以(也有能力)喚起前一個函式

解決方式:讓參數接受者去除該項參數,並直接呼叫前一個函式

作法:
(1)如果有必要,將參數的計算過程提煉到一個獨立函式中

(2)將函式本體內「對該參數的引用」替換為「對新建函式的呼叫」

(3)每次替換後,修改並測試

(4)全部替換完之後,使用《Remove Parameter》將該參數去掉


9.《Introduce Parameter Object》(引入參數物件)

情境:某些參數總是很自然地同時出現

解決方式:以一個物件取代這些參數

作法:
(1)新建一個 class,用以表現你想替換的一組參數。將這個 class 設為不可變的(immutable)

(2)編譯

(3)針對使用該組參數的所有函式,實施《Add Parameter》,以上述新建 class 之實體物件作為新添參數,並將此一參數值設為 null

*如果你所修改的函式被其他很多函式呼叫,那麼你可以保留修改前的舊函式,並令它呼叫修改後的新函式。你可以先對舊函式進行重構,然後逐一令呼叫端轉而呼叫新函式,最後再將舊函式刪除

(4)對於 Data Clump(資料泥團) 中的每一項(在此均為參數),從函式署名式中移除之,並修改呼叫端和函式本體,令它們都改而透過「新建的參數物件」取得該值

(5)每去除一個參數,編譯並測試

(6)將原先的參數全部去除之後,觀察有無適當函式可以運用《Move Method》搬移到參數物件之中

*被搬移的可能是整個函式,也可能是函式中的一個段落。如果是後者,先使用《Extrace Method》將該段落提煉為一個獨立函式,再搬移這一新建函式


10.《Remove Setting Method》(移除設值函式)

情境:你的 class 中的某個欄位,應該在物件初創時被設值,然後就不再改變

解決方式:去掉該欄位的所有設值函式

作法:
(1)檢查設值函式被使用的情況,看它是否只被建構式呼叫,或者被建構式所呼叫的另一個函式呼叫

(2)修改建構式,使其直接存取設值函式所針對的那個變數

*如果某個 subclass 藉由設值函式給 superclass 的某個 private 欄位設了值,那麼你就不能這樣修改。這種情況下你應該試著在 superclass 中提供一個 protected 函式(最好是建構式)來給這些欄位設值

無論你怎麼做,都不要給 superclass 中的函式取一個會與設值函式混淆的名字

(3)編譯,測試

(4)移除這個設值函式,將它所針對的欄位設為 final

(5)編譯,測試


11.《Hide Method》(隱藏某個函式)

情境:有一個函式,從來沒有被其他任何 class 用到

解決方式:將這個函式修改為 private

作法:
(1)經常檢查有沒有可能降低某個函式的可見度(使它更私有化)

*1 使用 lint-style 工具,盡可能頻繁地檢查。當你在另一個 class 中移除對某個函式的呼叫時,也應該進行檢查

*2 特別對設值函式進行上述的檢查

(2)盡可能降低所有函式的可見度

(3)每完成一組函式的隱藏之後,編譯並測試

*如果有不適當的隱藏,編譯器很自然會檢驗出來,因此不必每次修改後都進行編譯


12.《Replace Constructor with Factory Method》(以工廠函式取代建構式)

情境:你希望在創建物件時不僅僅是對它做簡單的建構動作

解決方式:將建構式替換為 factory method(此指工廠函式)

作法:
(1)新建一個 factory method,讓它呼叫現有的建構式

(2)將「對建構式的呼叫」替換為「對 factory method 的呼叫」

(3)每次替換後,編譯並測試

(4)將建構式宣告為 private

(5)編譯


13.《Encapsulate Downcast》(封裝「向下轉型」動作)

情境:某個函式傳回的物件,需要由函式呼叫者執行「向下轉型」動作

解決方式:將向下轉型動作移到函式中

作法:
(1)找出「必須對函式呼叫結果進行向下轉型」的地方

*這種情況通常出現在「傳回一個群集或迭代器(iterator)」的函式中

(2)將向下轉型動作搬移到該函式中

*針對傳回群集的函式,使用《Encapsulate Collection》


14.《Replace Error Code with Exception》(以異常取代錯誤碼)

情境:某個函式傳回一個特殊代碼,用以表示某種錯誤情況

解決方式:改用異常

作法:
(1)決定待拋異常應該是 checked 還是 unchecked

*1 如果呼叫者有責任在呼叫前檢查必要狀態,就拋出 unchecked 異常

*2 如果想拋出 checked 異常,你可以新建一個 exception class,也可以使用現有的 exception classes

(2)到該函式的所有呼叫者,對它們進行相應調整,讓它們使用異常

*1 如果函式拋出 unchecked 異常,那麼就調整呼叫者,使其在呼叫函式前做適當檢查。每次修改後,編譯並測試

*2 如果函式拋出 checked 異常,那麼就調整呼叫者,使其在 try 區段中呼叫該函式

(3)修改該函式的署名式,令它反映出新用法

如果函式有許多呼叫者,上述修改過程可能跨度太大。你可以將它分解成下列數個步驟:
(1)決定待拋異常應該是 checked 還是 unchecked

(2)新建一個函式,使用異常來表示錯誤狀況,將舊函式的程式碼拷貝到新函式中,並做適當調整

(3)修改舊函式的函式本體,讓它呼叫上述新建函式

(4)編譯,測試

(5)逐一修改舊函式的呼叫者,令其呼叫新函式。每次修改後,編譯並測試

(6)移除舊函式


15.《Replace Exception with Test》(以測試取代異常)

情境:面對一個「呼叫者可預先加以檢查」的條件,你拋出了一個異常

解決方式:修改呼叫者,使它在呼叫函式之前先做檢查

作法:
(1)在函式呼叫點之前,放置一個測試句,將函式內的 catch 區段中的程式碼拷貝到測試句的適當 if 分支中

(2)在 catch 區段起始處加入一個 assertion,確保 catch 區段絕對不會被執行

(3)編譯,測試

(4)移除所有 catch 區段,然後將 try 區段內的程式碼拷貝到 try 之外,接著再移除 try 區段

(5)編譯,測試




八. 處理概括關係

1.《Pull Up Field》(欄位上移)

情境:兩個 subclasses 擁有相同的欄位

解決方式:將此一欄位移至 superclass

作法:
(1)針對待提升之欄位,檢查它們的所有被使用點,確認它們以同樣的方式被使用

(2)如果這些欄位的名稱不同,先將它們改名,使每一個名稱都和你想為 superclass 欄位取的名稱相同

(3)編譯,測試

(4)在 superclass 中新建一個欄位

*如果這些欄位是 private,你必須將 superclass 的欄位宣告為 protected,這樣 subclasses 才能引用它

(5)移除 subclass 中的欄位

(6)編譯,測試

(7)考慮對 superclass 的新建欄位使用《Self Encapsulate Field》


2.《Pull Up Method》(函式上移)

情境:有些函式,在各個 subclass 中產生完全相同的結果

解決方式:將該函式移至 superclass

作法:
(1)檢查「待提升函式」,確定它們是完全一致的

*如果這些函式看上去做了相同的事,但並不完全一致,可使用《Substitute Algorithm》讓它們變得完全一致

(2)如果「待提升函式」的署名式不同,就將那些署名式都修改為你想要在 superclass 中使用的署名式

(3)在 superclass 中新建一個函式,將某一個「待提升函式」的程式碼拷貝到其中,做適當調整,然後編譯

*1 如果你使用的是一種強型(strongly typed)語言,而「待提升函式」又呼叫了一個「只出現於 subclass,未出現於 superclass」的函式,你可以在 superclass 中為被呼叫函式宣告一個抽象函式

*2 如果「待提升函式」使用了 subclass 的一個欄位,你可以使用《Pull Up Field》將該欄位也提升到 superclass;或者也可以先使用《Self Encapsulate Field》,然後在 superclass 中把取值函式宣告為抽象函式

(4)移除一個「待提升的 subclass 函式」

(5)編譯,測試

(6)逐一移除「待提升的 subclass函式」,直到只剩下 superclass 中的函式為止。每次移除之後都需要測試

(7)觀察該函式的呼叫者,看看是否可以將它所索求的物件型別改為 superclass


3.《Pull Up Constructor Body》(建構式本體上移)

情境:你在各個 subclass 中擁有一些建構式,它們的本體幾乎完全一致

解決方式:在 superclass 中新建一個建構式,並在 subclass 建構式中呼叫它

作法:
(1)在 superclass 中定義一個建構式

(2)將 subclass 建構式中的共同程式碼搬移到 superclass 建構式中

*1 被搬移的可能是 subclass 建構式的全部內容

*2 首先設法將共同程式碼搬移到 subclass 建構式起始處,然後再拷貝到 superclass 建構式中

(3)將 subclass 建構式中的共同程式碼刪掉,改而呼叫新建的 superclass 建構式

*如果 subclass 建構式中的所有程式碼都是共同碼,那麼對 superclass 建構式的呼叫將是 subclass 建構式的唯一動作

(4)編譯,測試

*如果日後 subclass 建構式再出現共同程式碼,你可以先使用《Extract Method》將那一部分提煉到一個獨立函式,然後使用《Pull Up Method》將該函式上移到 superclass


4.《Push Down Method》(函式下移)

情境:superclass 中的某個部分只與部分(而非全部) subclasses 有關

解決方式:將這個函式移到相關的那些 subclasses 去

作法:
(1)在所有 subclass 中宣告該函式,將 superclass 中的函式本體拷貝到每一個 subclass 函式中

*你可能需要將 superclass 的某些欄位宣告為 protected,讓 subclass 函式也能夠存取它們。

如果日後你也想把這些欄位下移到 subclasses,通常就可以那麼做;否則應該使用 superclass 提供的存取函式(accessord)。如果存取函式並非 public,你得將它宣告為 protected

(2)刪除 superclass 中的函式

*1 你可能必須修改呼叫端的某些變數宣告或參數宣告,以便能夠使用 subclass

*2 如果有必要透過一個 superclass 物件存取該函式,或如果你不想把該函式從任何 subclass 中移除,或如果 superclass 是抽象類別,那麼你就可以在 superclass 中把該函式宣告為抽象函式

(3)編譯,測試

(4)將該函式從所有不需要它的那些 subclasses 中刪掉

(5)編譯,測試


5.《Push Down Field》(欄位下移)

情境:superclass 中的某個欄位只被部分(而非全部) subclasses 用到

解決方式:將這個欄位移到需要它的那些 subclasses 去

作法:
(1)在所有 subclass 中宣告該欄位

(2)將該欄位從 superclass 中移除

(3)編譯,測試

(4)將該欄位從所有不需要它的那些 subclasses 中刪掉

(5)編譯,測試


6.《Extract Subclass》(提煉子類別)

情境:class 中的某些特性只被某些(而非全部)實體(instances)用到

解決方式:新建一個 subclass,將上面所說的那一部分特性移到 subclass 中

作法:
(1)為 source class 定義一個新的 subclass

(2)為這個新的 subclass 提供建構式

*1 簡單的作法是: 讓 subclass 建構式接受與 superclass 建構式相同的參數,並藉由 super 呼叫 superclass 建構式

*2 如果你希望對用戶隱藏 subclass 的存在,可使用《Replace Constructor with Factory Method》

(3)找出呼叫 superclass 建構式的所有地點。如果它們需要的是新建的 subclass,令它們改而呼叫新建構式

*1 如果 subclass 建構式需要的參數和 superclass 建構式的參數不同,可以使用《Rename Method》修改其參數列。如果 subclass 建構式不需要 superclass 建構式的某些參數,可以使用《Rename Method》將它們去除

*2 如果不再需要直接具現化(instantiated) superclass,就將它宣告為抽象類別

(4)逐一使用《Push Down Method》和《Push Down Field》將 source class 的特性移到 subclass 去

*1 和《Extract Class》不同的是,先處理函式再處理資料,通常會簡單一些

*2 當一個 public 函式被下移到 subclass 後,你可能需要重新定義該函式的呼叫端的區域變數或參數型別,讓它們改呼叫 subclass 中的新函式 

(5)找到所有這樣的欄位: 它們所傳達的資訊如今可由繼承體系本身傳達(這一類欄位通常是 boolean 變數或 type code)。以《Self Encapsulate Field》避免直接使用這些欄位,然後將它們的取值函式替換為多型常數函式(polymorphic constant methods)。

所有使用這些欄位的地方都應該以《Replace Conditional with Polymorphism》重構

*任何函式如果位於 source class 之外,而又使用了上述欄位的存取函式,考慮以《Move Method》將它移到 source class 中,然後再使用《Replace Conditional with Polymorphism》

(6)每次下移之後,編譯並測試


7.《Extract Superclass》(提煉超類別)

情境:兩個 classes 有相似特性

解決方式:為這兩個 classes 建立一個 superclass,將相同特性移至 superclass

作法:
(1)為原本的 classes 新建一個空白的 abstract superclass

(2)運用《Pull Up Field》、《Pull Up Method》和《Pull Up Constructor Body》逐一將 subclass 的共同元素上移到 superclass

*1 先搬移欄位,通常比較簡單

*2 如果相應的 subclass 函式有不同的署名式,但用途相同,可以先使用《Rename Method》將它們的署名式改為相同,然後再使用《Pull Up Method》

*3 如果相應的 subclass 函式有相同的署名式,但函式本體不同,可以在 superclass 中把它們的共同署名式宣告為抽象函式

*4 如果相應的 subclass 函式有不同的函式本體,但用途相同,可試著使用《Substitute Algorithm》把其中一個函式的函式本體拷貝到另一個函式中。如果運轉正常,你就可以使用《Pull Up Method》

(3)每次上移後,編譯並測試

(4)檢查留在 subclass 中的函式,看它們是否還有共通成分。如果有,可以使用《Extract Method》將共通部分再提煉出來,然後使用《Pull Up Method》將提煉出的函式上移到 superclass。

*如果各個 subclass 中某個函式的整體流程很相似,你也許可以使用《Form Template Method》

(5)將所有共通元素都上移到 superclass 之後,檢查 subclass 的所有使用者。如果它們只使用共同介面,你就可以把它們所索求的物件型別改為 superclass



8.《Extract Interface》(提煉介面)

情境:若干客戶使用 class 介面中的同一子集;或者,兩個 classes 的介面有部分相同

解決方式:將相同的子集提煉到一個獨立介面中

作法:
(1)新建一個空介面

(2)在介面中宣告「待提煉類別」的共通操作

(3)讓相關的 classes 實現上述介面

(4)調整客戶端的型別宣告,使之得以運用該介面


9.《Collapse Hierarchy》(摺疊繼承體系)

情境:superclass 和 subclass 之間無太大區別

解決方式:將它們合為一體

作法:
(1)選擇你想移除的 class: 是 superclass 還是 subclass?

(2)使用《Pull Up Method》和《Pull Up Field》,或者《Push Down Method》和《Push Down Field》,把想要移除的 class 內的所有行為和資料(欄位)搬移到另一個 class

(3)每次移動後,編譯並測試

(4)調整「即將被移除的那個 class」的所有引用點,令它們改而引用合併(摺疊)後留下的 class。這個動作將會影響變數的宣告、參數的型別,以及建構式

(5)移除我們的目標;此時的它應該已經成為一個空類別

(6)編譯,測試


10.《From Template Method》(塑造模板函式)

情境:你有一些 subclasses,其中相應的某些函式以相同的順序執行類似的措施,但各措施實際上有所不同

解決方式:將各個措施分別放進獨立函式中,並保持它們都有相同的署名式,於是原函式也就變得相同了。然後將原函式上移至 superclass

作法:
(1)在各個 subclass 中分解目標函式,使分解後的各個函式要不完全相同,要不完全不同

(2)運用《Pull Up Method》將各 subclass 內完全相同的函式上移至 superclass

(3)對於那些(剩餘的、存在於各 subclasses 內的)完全不同的函式,實施《Rename Method》,使所有這些函式的署名式完全相同

*這將使得原函式變為完全相同,因為它們都執行同樣一組函式呼叫;但各 subclass 會以不同方式回應這些呼叫

(4)修改上述所有署名式後,編譯並測試

(5)運用《Pull Up Method》將所有原函式一一上移至 superclass。在 superclass 中將那些「有所不同、代表各種不同措施」的函式定義為抽象函式

(6)編譯,測試

(7)移除其他 subclass 中的原函式,每刪除一個,編譯並測試


11.《Replace Inheritance with Delegation》(以委託取代繼承)

情境:某個 subclass 只使用 superclass 介面中的一部分,或是根本不需要繼承而來的資料

解決方式:在 subclass 中新建一個欄位用以保存 superclass;調整 subclass 函式,令它改而委託 superclass;然後去掉兩者之間的繼承關係

作法:
(1)在 subclass 中新建一個欄位,使其引用(指涉) superclass 的一個實體,並將它初始化為 this

(2)修改 subclass 內的每一個(可能)函式,讓它們不再使用 superclass,轉而使用上述那個「受託欄位」(delegate field)。每次修改後,編譯並測試

*你不能如此這般地修改 subclass 中「透過 super 呼叫 superclass 函式」的函式,否則它們會陷入無限遞迴。這一類函式只有在繼承關係被打破後才能修改

(3)去除兩個 classes 之間的繼承關係,將上述「受託欄位」的賦值動作修改為「賦予一個新物件」

(4)針對客戶端所用的每一個 superclass 函式,為它添加一個簡單的請託函式(delegate method)

(5)編譯,測試


12.《Replace Delegation with Inheritance》(以繼承取代委託)

情境:你在兩個 classes 之間使用委託關係,並經常為整個介面編寫許多極簡單的請託函式

解決方式:讓「請託(delegating) class」繼承「受託 class (delegate)」

作法:
(1)讓「請託端」成為「受託端」的一個 subclass

(2)編譯

*此時,某些函式可能會發生衝突: 它們可能有相同的名稱,但在回返型別(return type)、異常指定(exceptions)或可視性(visibility)方面有所差異。你可以使用《Rename Method》解決此類問題

(3)將「受託欄位」設為「該欄位所處之物件本身」

(4)去掉簡單的請託函式

(5)編譯並測試

(6)將所有其他「涉及委託關係」的動作,改為「呼叫物件本身(繼承而來的函式)」

(7)移除「受託欄位」




九. 大型重構

1.《Tease Apart Inheritance》(疏離並分解繼承體系)

情境:某個繼承體系(inheritance hierarchy)同時承擔兩個責任

解決方式:建立兩個繼承體系,並透過委託關係讓其中一個可以呼叫另一個

作法:
(1)首先識別出繼承體系所承擔的不同責任,然後建立一個二維表格,並以坐標軸標示出不同的任務。我們將重複運用本重構,處理兩個或兩個以上的維度。每次只處理一個維度

(2)判斷哪一項責任更重要些,並準備將它留在當前的繼承體系中。準備將另一項責任移到另一個繼承體系中

(3)使用《Extract Class》從當前的 superclass 提煉出一個新 class,用以表示重要性稍低的責任,並在原 superclass 中添加一個 instance 變數,用以保存新建 class 的實體

(4)對應於原繼承體系中的每個 subclass,創建上述新 class 的一個個 subclasses。在原繼承體系的 subclasses 中,將前一步驟所添加的 instance 變數初始化為新建 subclass 的實體

(5)針對原繼承體系中的每個 subclass,使用《Move Method》將其中的行為搬移到與之對應的新建 subclass 中

(6)當原繼承體系中的某個 subclass 不再有任何程式碼時,就將它去除

(7)重複以上步驟,直到原繼承體系中的所有 subclass 都被處理過為止。觀察新繼承體系,看看是否有可能對它實施其他重構手法,如《Pull Up Method》或《Pull Up Field》


2.《Convert Procedural Design to Objects》(將程序式設計轉化為物件設計)

情境:你手上有一些程式碼,是傳統程序式風格(procedural style)的寫法

解決方式:將資料記錄(data records)變成物件,將行為分開,並將行為移入相關物件之中

作法:
(1)針對每一個記錄型別(record type),將它轉變為「只含存取函式」的「啞資料物件」(dumb data object)

*如果你的資料來自關聯式資料庫(relational database),就把資料庫中的每個表(table)變成一個「啞資料物件」

(2)針對每一處程序式風格,將該處的程式碼提煉到一個獨立 class 中

*你可以把提煉所的 class 做成一個 Singleton(單件;為了方便重新初始化),或是把提煉所得的函式宣告為 static

(3)針對每一個長長的程序(procedure),實施《Extract Method》及其他相關重構,將它分解。再以《Move Method》將分解後的函式分別移到它所相關的啞資料類別(dumb data class)中

(4)重複上述步驟,直到原始 class 中的所有函式都被移除。如果原始 class 是一個完全程序式(purely procedural)的 class,就把它除掉


3.《Separate Domain from Presentation》(將領域和表述/顯示分離)

情境:某些 GUI classes 之中包含了 domain logic(領域邏輯)

解決方式:將 domain logic 分離出來,為它們建立獨立的 domain classes

作法:
(1)為每個視窗(window)建立一個 domain class

(2)如果視窗內有一張表格(grid),就新建一個 class 來表示其中的資料列(rows),再以視窗所對應之 domain class 中的一個群集(collection)來容納所有的 row domain objects

(3)檢查視窗中的資料。如果資料只被用於 UI,就把它留著;如果資料被 domain logic 使用,而且不顯示於視窗上,我們就以《Move Field》將它搬移到 domain class 中;如資料同時被 UI 和 domain logic 使用,就對它實施《Duplicate Observed Data》,使它同時存在於兩處,並保持兩處之間的同步

(4)檢查 presentation class 中的邏輯。實施《Extract Method》將 presentation logic 從 domain logic 中分開。一旦隔離了 domain logic,再運用《Move Method》將它移到 domain class

(5)以上步驟完成後,你就擁有了兩組彼此分離的 classes: presentation classes 用以處理 GUI,domain classes 內含所有業務邏輯(business logic)。此時的 domain classes 組織可能還不夠嚴謹,更進一步的重構將解決這些問題


4.《Extract Hierarchy》(提煉繼承體系)

情境:你有某個 class 做了過多的工作,其中一部分是以大量條件式完成的

解決方式:建立繼承體系,以一個 subclass 表示一種特殊情況

作法:

如果你無法確定變異應該是些什麼(也就是說你無法確定原始 class 中該有哪些條件邏輯)。這時候你希望每次一小步地前進:

(1)鑑別出一種變異

*如果這種變異可能在物件生命期內發生變化,就運用《Extract Class》將它提煉為一個獨立的 class

(2)針對這種變異,新建一個 subclass,並對原始 class 實施《Replace Constructor with Factory》。再修改 factory method(此指工廠函式),令它傳回適當的(相對應的) subclass 實體

(3)將含有條件邏輯的函式,一次一個,逐一拷貝到 subclass,然後在明確情況下(對 subclass 明確,但對 superclass 不明確),簡化這些函式

*如有必要隔離函式中的「條件邏輯」和「非條件邏輯」,可對 superclass 實施《Extract Method》

(4)重複上述過程,將所有變異(特殊情況)都分離出來,直到可以將 superclass 宣告為抽象類別為止

(5)刪除 superclass 中的那些「被所有 subclasses 覆寫」的函式(的本體),並將它宣告為抽象函式

如果你非常清楚原始 class 會有那些變異,可以使用另一種作法:
(1)針對原始 class 的每一種變異,建立一個 subclass

(2)使用《Replace Constructor with Factory Method》將原始 class 的建構式轉變成 factory method(此指工廠函式),並令它針對每一種變異傳回適當的 subclass 實體

*如果原始 class 中的各種變異是以 type code 標示,先使用《Replace Type Code with Subclasses》;如果那些變異在生命期間會改變,就使用《Replace Type Code with State/Strategy》

(3)針對帶有條件邏輯的函式,實施《Replace Conditional with Polymorphism》。如果非整個函式的行為有所變化,而只是函式一部分有所變化,請先運用《Extract Method》將變化部分和不變部分隔開來




十. 重構工具

1.Refactoring Browser

沒有留言:

張貼留言