🎯 什麼情境該想到我

當你「想測的是一個又大又長的方法,但它所在的類別在測試用具裡根本實例化不出來」的時候。

⚙️ 怎麼用(步驟)

先判斷該不該用(p.261)

  • 「在許多應用中,長方法都是非常難對付的角色。」如果能實例化包含這種方法的類別並將其放入測試用具,往往下一步便可以開始編寫測試了;但「有時候,為了讓一個類別能夠被獨立地實例化,需要付出相當大的努力:甚至可能對於你要進行的修改來說顯得有點得不償失了」。
  • 方法規模較小、且沒有使用實例資料 → 改用 暴露靜態方法(273頁)。
  • 方法規模較大,或使用了實例資料或其他方法 → 用分解出方法對象。

核心理念(p.261):「將一個長方法移至一個新類別中。後者的對象便被稱為方法對象,因為它們只含單個方法的程式碼。」通常運用之後就可以給新類別編寫測試,這會比為舊方法編寫測試容易一些;「舊方法中的局部變數可以做成新類別中的成員變數,這通常能令解依賴並改善程式碼狀況變得更容易」。

步驟(p.266,書中標明「以下步驟用於在沒有測試的情況下安全地分解出方法對象」)

  1. 建立一個將包含目標方法的類別。
  2. 為該類別建立一個建構函式,並利用簽名保持(249頁)手法來讓它具有跟目標方法完全一樣的參數列表。如果目標方法用到了原類別中的成員變數或方法的話,再往該建構函式的參數列表裡面加上一個對原類別的引用(添加為第一個參數)。
  3. 對於建構函式參數列表裡面的每個參數,建立一個相應的成員變數,型別分別與對應的參數型別完全相同。這一步仍可以利用簽名保持手法:將建構函式參數列表內的所有參數直接複製到成員變數宣告區段,並對格式作適當調整。在建構函式裡面對剛才建立的所有成員變數賦值或初始化。
  4. 在新類別中建立一個空的執行方法。通常該方法可以叫做 run,前面的例子中使用的是 draw。
  5. 將目標方法的方法體複製到剛才建立的執行方法中,然後編譯,依靠編譯器發現下一步所要作的修改。
  6. 編譯出錯資訊應當會告訴你該方法在哪兒使用了原類別的方法或成員變數。作相應改動以便令程式碼通過編譯。通常可以通過改用原類別的引用(指標)來呼叫其成員方法來達到目的。或者也有可能你需要將原類別中的相應方法置為公有;如果是成員變數,或許還得為其引入獲取方法函式,以免將其直接暴露出來。
  7. 新類別通過編譯之後,回到原先的目標方法,對其進行修改,讓它將工作全權委託給上面建立出來的方法對象(只需建立新類別的實例,然後呼叫其執行方法即可)。
  8. 如果需要,使用 介面提取(285頁)來解開對原類別的依賴。

三種變種(p.266)

  • 最簡單的:原方法不使用原類別中任何實例成員 → 無需傳原類別的引用給它。
  • 目標方法只使用原類別中的資料成員而不使用其方法 → 往往可以建立一個新類別,把被用到的資料成員放進去,然後傳遞該類別的對象給分解出的方法對象。
  • 「本節中展示的情況其實是最糟的一種:被分解出來的方法用到了原類別上的方法。因此我們用了介面提取,並在提取出的方法對象與原類別之間建立起一定程度的抽象。」

🧪 我實際套用的紀錄

  • (待填)

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

  • 它會逼你破壞封裝:「你可能感覺不爽,因為我們為了實現該技術,將原類別中的某些原本是私有的東西給暴露(公有)出來了。」書中例子裡原本私有的 drawPoint「現在就成了公有方法,完全暴露給了外界」。(p.265)
  • 結果看起來會很怪:「我們讓一個類別實現了一個新的介面,然而該介面的唯一一個使用者卻是一個由該類別建立出來的對象。」(p.265)
  • 但這不是結局:「隨著時間的推移,你會對沒法將 GDIBrush 放入測試用具感到越來越不滿,於是就會開始嘗試對其進行解依賴。等你成功將其放入測試用具之後,就會開始思考其他設計方案……一旦被測試覆蓋,我們便可以做許許多多其他的事情,所以最後的程式碼結構可能會大相徑庭。」(p.265)
  • 整章共通的誠實取捨:「這些技術並不能立竿見影地讓設計變得更好。實際上,如果你有良好的設計感,這裡的某些技術甚至可能會令你退縮。」(p.258)
  • 與《重構》的差別:書中註明本章手法雖與《重構》部分手法重疊,但「在步驟上有所不同,我將之適當剪裁以使得它們能夠被安全地用在沒有測試的情況下」(p.258)。所以別把它當成 提煉類別 或處理 過長函式 的做法——那些是在測試保護下改善結構,這裡的目的是先讓那個長方法能被寫出測試。

🔗 相關工具