🎯 什麼情境該想到我

當你「想測的那一簇方法跟擋住你建立實例的那些依賴其實毫無瓜葛」的時候。

⚙️ 怎麼用(步驟)

書的設定:有時候你要對付一個類別上面的一簇方法,而阻止你在測試用具中實例化這個類別的依賴卻又是跟這簇方法毫無瓜葛的。這裡所謂「毫無瓜葛」是指這些方法既沒有直接也沒有間接引用或觸碰到那些問題依賴。當然,這時你可以透過重複採用暴露靜態方法(275 頁)或分解出方法物件(261 頁)來「解決」問題,但那樣做未必就是解決這個問題的最直接辦法(p.304)。

做法:你可以將這簇方法(即所說的「特性」)提取出來,提升到一個抽象基底類別中。然後再對這個抽象基底類別進行子類化,並在測試中建立這個子類別的實例(p.304)。書中的例子是一個排程類別:想改的那個計算方法用到許多實例變數(所以暴露靜態方法不合適),而它又是個很小很小的方法(所以分解出方法物件似乎不值得,況且它對其他實例變數和方法的依賴會使我們被許多根本不想看到的麻煩纏住)——另一個做法就是將問題方法提升到一個基底類別中,把問題依賴(例如那些會呼叫資料庫的方法)留在原來的類別中,免得它們阻撓測試(p.305)。

書中列的步驟(p.307):

  1. 找出你想要提升到抽象基底類別中去的方法。
  2. 為它們建立一個抽象基底類別。
  3. 將這些方法轉移到該抽象基底類別中,再編譯。
  4. 編譯錯誤會告訴你這些方法引用到的其他實例成員,將它們也轉移到基底類別中。記住這麼做的時候要保持簽名,以盡量減少出錯的機會
  5. 當兩個類別都成功編譯之後,為那個抽象基底類別建立一個測試子類別,並往其中添加你覺得需要用於設置測試環境的方法。

為什麼基底類別非得是抽象的?(p.307 側欄)「實際上,是為了讓程式碼更易理解。設想你看到一塊程式碼基,那麼肯定會期望看到每個具體類別都被用到了;如果有一個具體類別沒有被任何程式碼直接用到(實例化)的話,你肯定會感到困惑,因為在你看來它們無異於『死程式碼』。」

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

書在示範完之後親自回顧了這個取捨(p.307):

  • 「那麼,這麼做是不是件好事呢?**從設計的角度來說,這種做法還不算理想。我們令一組特性跨越了兩個類別,這麼做的原因只不過是為了容易測試。**如果這兩個類別的特性之間的關係並不密切的話,這種特性跨越就可能會帶來混亂。」
  • 而書中例子正是如此:一個類別的職責是更新計劃項目,另一個提升出來的類別職責就廣了,包括取得計劃項目的預設時間、計算死時間等(p.307)。
  • 更好的重構方式其實是委託:「另一種好一點的重構方式是讓原類別委託某個驗證器物件,將存取資料庫的任務放在後者身上,但如果這一步目前看上去還太危險,或者說還有其他糟糕的依賴存在的話,那麼特性提升是個不錯的開始。」(p.307)
  • 降低風險的兩個配套:「使用特性提升手法時,你如果實施簽名保持(249 頁),並且依靠編譯器(251 頁)的話,風險就會小得多。」(p.307)
  • 這是暫時的,不是終點:「而當測試安置到位之後,我們可以再去考慮是否轉向委託的模式。」(p.307)
  • 與《重構》的分界:《重構》裡也有把成員往上搬的手法,但那是為了消除子類別間的重複、讓繼承體系更合理;這裡把方法提升上去的唯一理由是把問題依賴甩在原類別、讓要測的那簇方法能被單獨實例化。書明講這在設計上「還不算理想」,是為了測試而刻意拉開的一道縫。

🔗 相關工具