系統變更有兩種基本方式:Edit and Pray(編輯並祈禱)與 Cover and Modify(覆蓋並修改)。這兩種方式的差異,決定了你面對 legacy code 時的信心與效率。

Edit and Pray vs. Cover and Modify#

Edit and Pray#

這是業界的普遍做法:仔細規劃變更、確保理解要修改的程式碼、小心翼翼地進行修改,然後四處檢查確認沒有破壞任何東西。表面上看,「小心工作」很專業,但安全性不僅僅是小心的函數

沒有人會選擇一位用奶油刀動手術的醫生,僅僅因為他「很小心」。有效的軟體變更和有效的手術一樣,需要更深層的技能和正確的工具

Cover and Modify#

另一種方式是在變更前先建立安全網(Safety Net)——用測試覆蓋我們要修改的程式碼。這個安全網不是防止我們跌倒的墊子,而是像披在程式碼上的防護罩,確保壞的改變不會洩漏出去感染其他部分。

當我們有一組好的測試環繞著程式碼,就能快速發現變更的效果是好是壞。我們仍然需要同樣的細心,但透過回饋機制,能更有信心地做出改變。

什麼是單元測試#

單元測試(Unit Test) 是針對系統中最小的行為單元進行的隔離測試。在程序式程式碼中,單元通常是函式;在物件導向程式碼中,單元是類別。

大型測試的問題#

雖然大型測試(整合測試、端對端測試)很重要,但它們有幾個固有的問題:

  • 錯誤定位困難:測試離被測程式碼越遠,失敗時越難判斷問題出在哪裡
  • 執行時間過長:大型測試執行緩慢,跑太久的測試最終會被人跳過不跑
  • 覆蓋度不足:難以看出程式碼與測試之間的對應關係,新增程式碼時可能需要大量工作來建立對應的高階測試

良好單元測試的特質#

  1. 執行速度快
  2. 能快速定位問題

執行時間超過 1/10 秒的單元測試就算是慢的單元測試。以一個 3,000 個類別的專案為例,若每個類別 10 個測試,共 30,000 個測試。以 1/10 秒計算需要近一小時;以 1/100 秒計算只需 5-10 分鐘。

什麼不算單元測試#

以下情況的測試不是單元測試:

  1. 連接資料庫
  2. 透過網路通訊
  3. 存取檔案系統
  4. 需要特殊環境設定才能執行

這些測試不一定不好,它們通常值得撰寫,也通常在單元測試框架中執行。但重要的是將它們與真正的單元測試分開,以維持一組可以快速執行的測試。

更高層級的測試#

單元測試很重要,但高層級測試(Higher-Level Tests)也有其價值。高層級測試可以用來驗證一組類別之間的協作行為,也常作為撰寫個別類別測試的起點。

測試覆蓋(Test Coverings)#

在 legacy 專案中開始做改變時,最安全的做法是先為要修改的程式碼加上測試。但這立刻帶來一個矛盾:

Legacy Code 的困境:修改程式碼前應該先有測試;但要加入測試,往往得先修改程式碼(打破相依性)。

相依性問題#

當類別直接依賴於難以在測試中使用的東西(如資料庫連線、Servlet、硬體裝置),就很難將它們放入測試框架中。解決這個問題的核心手段是打破相依性(Breaking Dependencies)

  • 引入介面(Interface)來替代具體的實作
  • 透過參數傳遞取代硬編碼的相依物件
  • 使用 Primitivize ParameterExtract Interface 等重構技術

Figure 2.1: Invoice update classes

Figure 2.2: Invoice update classes with dependencies broken

打破相依性以建立測試時,有時會讓程式碼看起來「更醜」。這就像手術的切口——表面上留了疤痕,但底下的一切都變得更健康了。日後當你能為這些區域加上更多測試時,連疤痕都可以癒合。

Legacy Code 變更演算法#

面對 legacy code 需要做變更時,可以遵循以下五個步驟:

  1. 辨識變更點(Identify Change Points)
  2. 找到測試點(Find Test Points)
  3. 打破相依性(Break Dependencies)
  4. 撰寫測試(Write Tests)
  5. 進行變更與重構(Make Changes and Refactor)

每次程式設計的目標不只是做出功能性變更,更要讓更多系統被測試覆蓋。隨著時間推移,被測試覆蓋的區域會像從海面升起的島嶼,逐漸擴大為穩固的大陸。

本書接下來的兩章將介紹 legacy code 工作中三個關鍵概念的背景知識:感測(Sensing)分離(Separation)接縫(Seams)