🎯 什麼情境該想到我

當你「要把某個類納入測試,但它相依的那個具體類別在測試裡建不出來,你需要傳一個偽物件進去」的時候。

書中 p.285 給出定位與原理:「在許多語言中介面提取都是最安全的解依賴技術之一。如果某步出錯了,編譯器就會立即告訴你,所以說用介面提取的時候,極少可能引入 bug。介面提取的關鍵在於為你的類建立一個介面,該介面包含你想要在某些上下文中使用的所有方法。完成介面之後,就可以讓你的類實現它,從而通過該介面來感知或分離(比如傳遞一個偽物件給你想要測試的類)。」

p.285 說介面提取有三種方式:

  • 用現成的自動重構工具(若 IDE 支援)。書中說這類工具「往往會允許你選中一個類上的某些方法然後鍵入新介面的名字」,更好的還會問你要不要順手把程式碼中某些地方改為使用新介面,「會幫你節省可觀的工作量」。
  • 一步一步手工提取,也就是下面的步驟;這是書中重點描述的方式。
  • 一次性從類裡面剪下/複製出多個方法,然後放到一個新的介面中。p.285:「這種做法雖然沒有前兩種做法安全,但仍然還算是相當不錯的;而且如果沒有重構工具支援並且建置耗時很長的話,該方式往往是唯一的選擇。」

⚙️ 怎麼用(步驟)

書中 p.289「步驟」小節:

  1. 建立一個新介面,給它起一個好名字。暫時不要往裡面添加任何方法。
  2. 令你提取介面的目標類實現該介面。這一步不會破壞任何東西,因為介面上還沒有任何方法。但你也可以編譯確認一下。
  3. 將你想要使用偽物件的地方從引用原類改為引用你新建的介面。
  4. 編譯系統。如果編譯器回報介面上缺少某某方法,則添加相應的方法(同時也往偽類上面添加一個空的實現),直到編譯通過。

p.288 描述這個流程實際跑起來的樣子:前幾步做完後「所有程式碼應當仍然完全能夠通過編譯,因為我們所做的只不過是引入了幾個新類,並讓一個既有類實現了一個空的介面」;接著才是真正的重構——把想要使用介面的地方改為對介面的引用,然後「編譯程式碼,並從編譯錯誤中發現許多地方呼叫了⋯⋯上的方法,我們將相應方法加到⋯⋯介面中來解決這些編譯錯誤,同時也將該方法的一個空的實現加到偽類中」。

🧪 我實際套用的紀錄

  • (待填)

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

  • 與《重構》的「提煉介面」不是同一件事。 兩者外形相似、動機不同:重構是在測試保護下改善結構;這裡的介面提取是為了能寫出測試而打破依賴——p.285 明說目的是「通過該介面來感知或分離(比如傳遞一個偽物件給你想要測試的類)」,是先有測試的需求才動這一刀。這個動機差異直接決定了下面那條「不必提取所有公有方法」的做法。
  • 不要貪心提取所有公有方法。 p.288 用一段對話點破:讀者會問「不是吧,我們還沒有往介面以及偽類上添加 recordError 方法呢」,作者回答那個方法確實存在,「但實際情況是,我們的測試並不需要這個方法。儘管,把一個類的所有公有方法都放到介面上是個不錯的做法,但如果順著這條路走下去,就有可能要做許多不必要的工作才能將程式碼最終納入測試了。」p.289 邊註總結:「提取介面時並不一定要提取類上的所有公有方法。你可以依靠編譯器來幫你發現哪些方法需要加到介面上去。」
  • 設計上真的需要完整介面就遞增式來。 p.289:「如果你覺得程式碼的設計走向的確要求介面擁有相應類上的所有公有方法的話,不妨考慮遞增式地擴充該介面。許多時候,在得到足夠的測試覆蓋之前,最好避免大規模的改動。」
  • 唯一的困難是非虛擬方法。 p.289:「該手法唯一的困難之處在於當面對非虛擬方法的時候。這裡所謂的非虛擬方法在 Java 裡面可能是靜態方法,而在 C# 或 C++ 裡面則可能是非虛擬成員函式。」p.289–290 的附註說明:在允許非虛擬實例方法的語言中,如果類沒有衍生類別,可以把目標方法設為虛的照常提取;但如果類有衍生類別,把它的非虛擬函式設成虛的並放到介面中就會破壞既有程式碼的行為——C++ 中原本靜態決議到基類的呼叫,提取介面後會改為呼叫衍生類別的版本。書中並指出「在 Java 或 C# 中並沒有這個問題。Java 中所有的實例方法都是虛的。而 C# 中的情況則要安全一點,因為添加一個介面並不會影響到目前對非虛擬方法的呼叫。」
  • 遇到「真的需要通過介面存取某個類上的非虛擬方法、而這個類又有衍生類別」時,p.290 給的最佳做法是添加一個新的虛擬方法,讓它委託呼叫那個非虛擬甚至靜態的方法,並確保該方法對所有衍生類別都做了正確的事。
  • 命名是主要的摩擦點。 p.287「介面命名」附註反對無腦加 “I” 前綴:好處是不用動腦筋,「但也有它的壞處,那就是你最終會發現許多程式碼都不知道對付的究竟是一個介面還是一個類」;而且之後想把帶 “I” 前綴的介面做成普通類「就會引發大規模的改動。而如果不改的話,那個名字又會作為一個微小的謊言躺在程式碼中」。同頁建議:如果手頭缺少能夠自動重新命名類的工具,「最好在使用該介面的程式碼還沒有大量出現之前給它起個穩定的名字」。

🔗 相關工具