🎯 什麼情境該想到我
當你今天就得在一段沒有測試的舊程式上加功能,而把它整個放進測試用具的代價你現在付不起時——用「寫全新的程式碼」繞過去,至少讓新加的那部分是被測試過的。
書裡的場景(p.51):
老闆走了進來,說「客戶們嚷著要這個特性,今天能完成嗎?」……你檢查了一下手頭的項目,有現成的測試嗎? 沒有。
書也提醒,正因為進度壓力才更要小心:「當處在期限壓力之下時,決定是否編寫測試就成了個難題,最困難的地方就在於你可能並不知道添加某個特性需要花多少時間。」(p.51)
⚙️ 怎麼用
先做一次判斷(p.51)
書給的順序是:
- 先試著在測試用具中實例化這個類。若不能,去讀書中談解依賴的那兩章。
- 「如果在了解這兩章所提到的方法之後還是覺得實在沒法承受現在就去解依賴並安置好測試的代價的話,那就仔細分析一下你所要進行的修改。可以通過編寫全新的代碼來完成它嗎? 很多時候這是可行的。」(p.51)
- 可行的話,才進入下面四種手法。
四種手法
| 手法 | 做什麼 | 什麼時候用 |
|---|---|---|
| 新生方法(Sprout Method,p.53) | 把新功能寫成一個新方法,在原方法的修改點呼叫它 | 新功能可以寫成一塊獨立的程式碼、原方法還能進測試用具 |
| 新生類(Sprout Class,p.57) | 建另一個類來容納這次改動,在原類中使用它 | 原類根本無法在合理期限內放進測試用具;或這次改動等於給原類加一個全新職責 |
| 外覆方法(Wrap Method,p.58) | 把原方法改名,用原名建一個新方法,新方法呼叫改名後的舊方法+新行為 | 想為原方法的既有呼叫一次補上行為 |
| 外覆類(Wrap Class,p.61) | 建一個新類持有原物件,在新類中做新行為,其餘委託給原物件 | 已存在一堆對該方法的呼叫;或原類已經大到不想再撐大 |
新生方法的步驟(p.53–54):確定修改點 → 在修改點插入一個對「還沒寫的新方法」的呼叫,先把這行註解掉 → 確定新方法需要原方法的哪些區域變數,當作實參傳進去 → 確定新方法要不要回傳值給原方法 → 用測試驅動開發寫出新方法 → 把註解掉的呼叫重新生效。
新生類的步驟(p.58)同構:改用一個類來完成、取個恰當的名字,在修改點插入建立物件與呼叫的程式碼並先註解掉,需要的區域變數改成傳給建構函數。
外覆方法第一形式的步驟(p.60–61):確定待修改的方法 → 把它重新命名,並用它原先的名字和簽名建立一個新方法(書特別提醒「記住要簽名保持」)→ 在新方法中呼叫改名後的原方法 → 為新特性寫一個方法(測試先行),在新方法中呼叫它。
第二形式(p.61):不必沿用舊名字——用 TDD 寫一個承載新特性的新方法,再建另一個函式去呼叫新舊兩個方法,讓使用者「可以在兩種支付方式之間自由選擇」(p.60)。
外覆類的步驟(p.65):確定修改點 → 新建一個類,其建構函數接受被外覆的物件;「如果你無法在測試用具中創建外覆類的實例的話,你可能需要先對被覆類使用實現提取或接口提取技術」→ 用 TDD 寫承載新行為的方法,再寫一個方法去呼叫它和被覆類中的舊方法 → 在需要新行為的地方建立並使用外覆類的物件。
怎麼選(p.65)
- 新生方法 vs 外覆方法「區別是相當細微的」:
通常,如果現有方法中的代碼將一個清晰的算法傳達給了讀者,我就會使用新生方法。而如果我覺得欲添加的新特性的重要性跟已經存在的特性不相上下,就會轉而使用外覆方法。
- 外覆類的「使用閾值」要更高一些,兩種情況才用:(1)「欲添加的行為是完全獨立的,並且我們不希望讓低層或不相關的行為污染現有類」;(2)「原類已經夠大了,我實在不能想像把它撐得更大會如何」——這種情況下「使用外覆類只是相當於在地上插個木樁,為後面的修改設下一個標識」。
- 外覆類有兩種味道(p.62、p.64):與被外覆類具有相同接口的那種,書說「該技術在設計模式裡面被稱作裝飾模式」,好處是「可以透明地一次性將新的行為添加到一組……現存調用上」;若「只需要將新的行為添加到少數幾個地方」,則建一個非「裝飾性」的外覆類。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
共同代價一:測不到呼叫端。 這是全章最重的警告(p.51)——
當你用這些技術時,雖然是在系統中添加已被測試的代碼,然而除非你用測試覆蓋了所有調用這些代碼的代碼,否則還是沒有對這些代碼的使用進行測試。所以,小心使用。
共同代價二:等於暫時放棄原方法/原類。 新生方法的缺點(p.54):
效果上等於暫時放棄了原方法以及它所屬的類,也就是說你暫時不打算將它們置於測試之下和改善它們了……原方法可能包含了大量複雜的代碼以及一個新生方法。有時候事情並不明朗,為什麼偏偏就那點工作要放到其他地方去呢?
共同代價三:暫時讓設計變差。 新生類的主要缺點(p.58)是「可能會使系統中的概念複雜化」,「開始破壞系統中原有的抽象,並將大批工作放在其他類中進行……理想情況下應當呆在原有類中的代碼最終卻棲身在新生的類當中,實屬無奈之舉」。書自己也預演了讀者的白眼(p.56):「就為這點小事去創建一個新類也太荒唐了?」——然後回答「之所以這麼做,唯一的目的就是為了擺脫一個惡劣的依賴環境」。
外覆方法特有的兩個缺點(p.60、p.61):
- 「你添加的新特性無法跟舊特性的邏輯『交融』在一起。它們要麼在舊特性之前要麼在之後完成。」書說這其實並非壞事,建議儘量這麼做。
- 「可能會導致糟糕的命名」——你被迫替原方法裡的舊程式碼取一個新名字,而「許多時候我們之所以進行方法外覆正是因為缺少相應的測試,代碼脆弱且沒有上述工具(安全的方法提取重構工具)」。
外覆方法相對省事的地方(p.61):「新生方法和新生類都會將代碼添加到現有方法中,至少增加一行,而外覆方法則不會增加現有方法的體積。」
裝飾別套太多層(p.64):「裝飾是個不錯的模式,但還是保守使用比較好。在一個裝飾類套一個裝飾類的代碼中『行走』就好像是在一層一層地剝洋蔥皮一樣。」
新生方法卡住時的退路(p.54):若為了測試新生方法得先「偽造一大堆它的構造函數的實參」,替代方案是傳 Null 技術;再不行就「考慮將新生方法設為一個公用靜態方法」,把原類的實例當實參傳進去。書把靜態方法看成「臨時場地」,等它們數量累積、共用變數時再抽成新類。
別忘了這是欠款不是解法(p.66 小結):
之所以去創建一個新類唯一的原因就是我們想要編寫受測試的新代碼,而目前還沒有時間去將那個現有類納入測試。這是個很現實的情況。
書說後續會發生的事是:「一段時間之後,你對於總是避開舊類龐大的身軀感到厭煩了,於是開始試圖將它納入測試中」——而且因為你一直在查看那個巨大的未測試類來決定從哪裡抽生新類,「把它納入測試也就變得越來越不可怕了」。另外書也提醒同事關的社會成本(p.65):只為一個小特性就外覆一個類,若給不出有說服力的理由,可能會被同事「優待」一番。
🔗 相關工具
- 工具-遺留程式碼修改流程 —— 同書的完整流程;本卡是「來不及走完整套」時的捷徑
- 工具-打破依賴以便測試 —— 書要你先試這條路,真的付不起代價才退回本卡
- 工具-特徵測試 —— 等原類終於進得了測試用具時,用它把舊行為鎖住
- 回連 修改程式碼的藝術