🎯 什麼情境該想到我

當你「要測的程式碼到處都在用同一組全域變數或全域自由函式,而測試時換不掉它們」的時候。

⚙️ 怎麼用(步驟)

三個選項(p.268):「在測試依賴於全域變數的程式碼時本質上有三個選擇:想辦法讓它依賴的全域變數在測試期間具有另一種行為;利用連接器,連接到另一個全域變數定義;或將它封裝起來,從而可以進一步進行解耦。最後一個選擇稱作封裝全域引用。」

為什麼不用參數化方法就好(p.268):「如果 suspend_frame 呼叫了某個函式,而後者也使用了該全域變數的話,我們就必須同樣將該變數作為參數傳遞給它。」

判準(p.269):「如果若干全域變數總是被一起使用一起修改,則它們應屬同一個類別。」書中建議先檢查這些全域變數還在哪些地方被用到,如果每次都是一起用,就把它們綁在一起,建立一個「智能」的類別來持有它們。

步驟(p.272–273)

  1. 找出有待封裝的全域變數/函式。
  2. 為它們建立一個類別(你將通過該類別來引用它們)。
  3. 將全域變數/函式複製到該類別中。如果其中有些是變數,別忘了在類別的建構函式中進行適當的初始化。
  4. 將全域變數/函式的原始宣告註解掉。
  5. 宣告新類別的一個全域對象。
  6. 依靠編譯器(251頁)幫你找出所有用到了這些全域變數/函式的地方。
  7. 將所有對它們的引用加上剛建立的那個新類別的全域對象為前綴。
  8. 在想要使用偽物件的地方,利用 引入靜態設置方法(292頁)、參數化建構函式(297頁)、參數化方法以獲取方法替換全域引用

命名(p.269)

  • 「命名一個類別的時候,考慮最終會位於它裡面的方法。當然我們應當給它起一個好名字,但並不一定是完美的。別忘了,你總是可以重新命名它的。」
  • 「你想到的類名可能已經被用掉了。這時候可以考慮重新命名那些使用了該名字的實體,從而將該名字騰出來。」
  • 搬移過去時「我們故意保留了這兩個陣列原來的名字,這是為了簡化後面的步驟」(p.269);等傳遞成功之後再做一點小小的重新命名讓程式碼清晰一些(p.270)。

也適用於全域自由函式(p.271–272)

  • 「不僅是全域資料,對於 C++ 中的非成員函式(也稱自由函式),也可以做同樣的事情。」面對 C API 時「你唯一的接縫就是被呼叫函式的連接期接縫」;可以用 連接替換(296頁),但用封裝全域引用建立另一種接縫「就能得到更佳的程式碼結構」。
  • 做法:「要封裝對全域自由函式的引用,只需建立一個介面類別,然後從它派生出偽類及產品類別。產品類別中的程式碼什麼都不用做,只需直接委託/呼叫相應的全域函式即可。」(p.272)
  • 為什麼要做成虛的:也可以改用「自由函式委託給其他自由函式」或「做成新類別的靜態函式」,「但這兩種方案都不能提供良好的接縫」,那樣就不得不改用連接期接縫或預編譯期接縫(29頁);「若是使用類別+函式方案並輔以參數化類別的話,引入的接縫就既明顯又易於掌控了」(p.272)。

🧪 我實際套用的紀錄

  • (待填)

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

  • 中途程式碼會變醜,這是預期內的:「完成這些之後,你會發現程式碼更加醜陋了,但重要的是它能正確編譯執行,所以說前面的步驟是保持行為的。」(p.270)
  • 只是第一步,不是終點:「從引用一個簡單的全域變數到引用一個類別成員只是第一步。之後你還需要考慮是否應當使用引入靜態設置方法或參數化建構函式,又或者參數化方法。」(p.270)「看起來改進不大,是不是?但實際上這是非常有價值的第一步。」(p.271)
  • 刻意不做「乾淨」的版本:書中自問為什麼把兩個全域變數放進新類別還設成公有——「凡事不宜操之過急。更何況手頭還沒有測試,而且我們要花盡量少的工作先將測試安置到位再說,這時候最好能不碰就不去碰程式碼中的邏輯。我們應當盡量避免觸動這些邏輯,並嘗試往程式碼中引入接縫,以便通過這些接縫來植入測試用的程式碼或資料。」(p.270)
  • 搬移的順序:「在使用封裝全域引用手法時,從資料或小型方法開始著手。稍大一點的方法可以等測試到位之後再移至新類別中。」(p.270)
  • 有些全域變數搬不進既有類別:書中例子中,把兩個陣列做成 AGGController 的成員變數行不通,因為還有其他類別也要用到它們(p.268)。
  • 與《重構》的差別:這條和 封裝變數全域資料 名字相近但目的不同——書中註明本章手法「在步驟上有所不同,我將之適當剪裁以使得它們能夠被安全地用在沒有測試的情況下」(p.258);這裡封裝是為了造出一個接縫好把測試安置進去,不是為了改善設計。

🔗 相關工具