🎯 什麼情境該想到我

當你「想在測試裡把某個成員換成假的,但那個成員是具體型別、而繼承這條路又不好走」的時候。

書上的定位:「本章提到的許多解依賴技術都依賴於物件導向的核心機制,如介面以及實現繼承。而有些新的語言特性則提供了另外的選擇。例如,如果你的語言支援泛型以及型別別名,則可以使用叫做模板重定義的手法來解依賴。」(p.320)

書中的例子是 C++ 的 AsyncReceptionPort,它有個具體的 socket 成員:「如果我們想修改 Run() 函式中的邏輯,就會發現要想在測試工具中執行該方法,就必須通過一個 socket 發送東西。在 C++ 中我們可以通過把 AsyncReceptionPort 做成一個類別模板來完全避免這個問題。」(p.321)改完之後,測試檔案裡就能用一個 FakeSocket 來實例化該類別模板(p.321)。

typedef 是這招最漂亮的地方: 「該技術最漂亮之處就在於我們可以通過一個 typedef 來避免在程式碼庫中到處修改對該類別的使用。如果沒有 typedef 的話,就需要將每處對 AsyncReceptionPort 的使用換成 AsyncReceptionPort<CSocket>。這就意味著大量無聊的工作,但難倒是不難,我們可以依靠編譯器(251 頁)來確保修改了每一處地方。在支援泛型但不支援型別別名機制(如 typedef)的語言中,你只能依靠編譯器。」(p.322)

⚙️ 怎麼用(步驟)

書中特別聲明:「以下是在 C++ 中運用模板重定義手法的步驟。在其他支援泛型的語言中步驟或許有所不同,但原則一樣。」(p.322)

  1. 在待測試類別中找出你想要替換的特性。
  2. 將該類別做成一個類別模板,根據你想要替換的變數對它進行參數化,將方法體轉移到標頭檔中。
  3. 給該類別模板另起一個名字。可以將原類別名後面加上「Impl」。
  4. 在類別模板定義之後加上一行 typedef,把「Impl 版本以具體型別實例化後的結果」重新命名回原類別名(書中寫成 typedef XXXImpl<T> XXX; 的形式,其中 XXX 是原類別名,T 為參數化型別的具體型別)。
  5. 在測試檔案中包含該類別模板的定義,用新的測試用型別來實例化。

🧪 我實際套用的紀錄

  • (待填)

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

  • ⚠️ 語言限制:需要語言同時支援泛型與型別別名。 書中示範用的是 C++(p.320–323)。只有泛型、沒有型別別名(typedef 這類)的語言仍可用,但你就得靠編譯器一處一處改過去(p.322)。

  • ⚠️ 主要缺點:實作被逼進標頭檔,依賴變多、重編譯變慢。 書中的側欄講得很直接(p.322):

    「C++ 中的模板重定義手法有一個主要的缺點,即當你參數化一個類別之後,它的實作程式碼就必須轉移到標頭檔中來。這會增加系統中的依賴。每次修改類別模板的程式碼之後,使用該類別的程式碼都必須重編譯。」

    (簡體版譯者在此加註:C++ 編譯器普遍不支援模板的分離編譯。)

  • Feathers 自己不把它當首選。 「一般來說我仍然傾向於採用基於繼承的手法在 C++ 中解依賴。然而如果想要解開的依賴本就處於模板程式碼中的話,該手法就可以用了。」(p.322)書中舉的正面例子是 CollaborationManager 這種本來就是模板的類別——「考慮到在這裡模板的使用方式使得介面提取比較困難,我們可以換種方式來參數化 CollaborationManager,問題就迎刃而解了。」(p.322)

  • 拿它來替換「方法定義」是下策。 「在 C++ 中你甚至可以利用該技術來替換方法的定義,只不過這麼做就有點不夠優雅了。C++ 的語言規則要求你必須提供一個模板參數,所以可以選擇一個成員變數並將其型別泛化為模板參數,或引入一個新的成員變數以便能夠基於某個型別來參數化你的類別——但我非到萬不得已是不會採取這種做法的。我會先非常謹慎地考察是否能使用基於繼承的技術。」(p.322)
    (簡體版譯者在此加註,認為作者這段敘述似乎有問題:實際上在 C++ 中一個類別模板的模板參數並不一定要在該類別模板裡面使用到。)

  • 它會改變型別的名字。 步驟 3、4 要你把原類別改名加 Impl 再用 typedef 把原名補回去——原名還在,但它現在是別名而不是類別本身。

🔗 相關工具