🎯 什麼情境該想到我

當你「在 C++ 裡想覆寫建構函式中被呼叫的方法,卻發現覆寫根本沒生效」的時候。

書上的起點:「建構函式中的物件建立可能會帶來依賴問題,尤其是當測試很難依賴這些物件的時候。大多數情況下我們可以使用提取並重寫工廠方法(276 頁)手法來對付這個問題。但對於那些不支援在建構函式中呼叫虛擬函式的語言,則必須另覓他法。而辦法之一便是本節所要講的替換實例變數。」(p.317)

為什麼子類化並重寫方法在這裡會失效

書中用 Pager 的例子示範這個陷阱:建構函式裡呼叫了虛擬函式 formConnection,「由於 formConnection 是個虛擬函式,所以人們可能會想當然地以為只要對它進行子類化並重寫方法(314 頁)就夠了」(p.317)——但驗證下來不是。

「在 C++ 中重寫虛擬函式時,基底類別相應的行為會被派生類別中的行為替換,這一點跟我們預期的一樣,然而有一點例外,就是當你在建構函式中呼叫一個虛擬函式時,呼叫並不會被分發到派生類別中的相應虛擬函式上去。」(p.318)結果就是那次 formConnection 的呼叫被決議到基底類別 Pager::formConnection 上。

C++ 為什麼要這條規則?書中的解釋是:若基底類別 A 的建構函式呼叫了被 B 覆寫的方法,而 B 的覆寫版本用到 B 自己的成員——「既然還沒輪到 B 的建構函式,那麼就是說該成員根本就還沒被初始化,結果可想而知」(p.318)。

「一些其他語言在這個問題上則要放鬆一些,比如 Java 中允許這麼做,但我不建議你在產品程式碼中這麼幹。」(p.318)

什麼時候用它、什麼時候用別的

書中給了明確的分岔(p.319):

  • 如果你想要替換的物件並沒有在建構函式中被使用(而只是建立的話)→ 採用提取並重寫獲取方法。
  • 如果建構函式中使用了該物件,並且你需要確保在另一個方法被呼叫之前將該物件替換掉 → 採用替換實例變數。

⚙️ 怎麼用(步驟)

書中列出的步驟(p.320):

  1. 找出你想要替換的實例變數。
  2. 建立一個名為 supersedeXXX 的方法,其中 XXX 是你想要替換的變數的名字。
  3. 在該方法中銷毀原先被建立出來的那個物件,換入你新建出來的物件。如果持有該物件的實例成員是一個引用,則需要確保該類別中沒有其他成員引用了原先建立出來的那個物件。如果有的話,你可能就需要在 supersedeXXX 方法裡面多做一點工作,來確保能夠安全地換入你的新物件並且確保達到正確的效果。

使用方式(書中原話是針對 BlendingPen 的例子):在測試時,可以根據需要建立該物件,並在需要放入感知物件的時候呼叫它的 supersede... 方法。(p.319)

為什麼名字要叫 supersede: 「使用 supersede 作為這類方法的方法名前綴的一個好處就是這個單詞比較奇異且不常見,於是如果你擔心別人會在產品程式碼中使用這個方法的話,只需搜尋一下 supersede 就能知道結果了。」(p.319)

🧪 我實際套用的紀錄

  • (待填)

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

  • Feathers 自己都承認這招看起來很糟。 「從表面上來說,替換實例變數看起來是個挺糟糕的放置感知變數的手法,但在 C++ 中,當參數化建構函式(297 頁)手法由於建構函式中糾纏的邏輯而變得難以使用時,替換實例變數就成了最好的選擇。不過,在允許建構函式中呼叫虛擬函式的語言中,提取並重寫工廠方法(276 頁)通常是更好的選擇。」(p.319)——它是被語言限制逼出來的選項,不是首選。

  • ⚠️ 它在產品程式碼上留下一個設置方法,而這是不良實踐。 書中特別開了一個側欄講這個代價(p.319):

    「一般而言,提供設置方法來允許外界修改被用到的子物件屬於不良實踐。這些設置方法允許客戶程式碼徹底改變一個物件在其生命週期當中的行為。在旁人可以呼叫這些設置方法時,你就必須得了解目標物件的歷史狀態,方能知道對它的方法呼叫會帶來什麼後果。而當沒有設置方法時,程式碼便更易於理解。」

    也就是說:你為了能測試,主動在設計上加了一個會讓程式碼變得更難理解的口子。supersede 這個罕見字首正是用來監控這個口子有沒有被濫用。

  • 替換時要處理好舊物件的所有權。 步驟 3 明講:如果是引用,得確認類別中沒有其他成員還引用著舊物件(p.320)。

  • 不要為了控制工廠而動用引入靜態設置方法。 書中在 BlendingPen 的例子裡評估過:可以用引入靜態設置方法來控制工廠產出的物件,「但這麼改動的話就太具侵入性了」(p.319)。

🔗 相關工具