系統變更有兩種基本方式: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/10 秒的單元測試就算是慢的單元測試。以一個 3,000 個類別的專案為例,若每個類別 10 個測試,共 30,000 個測試。以 1/10 秒計算需要近一小時;以 1/100 秒計算只需 5-10 分鐘。
什麼不算單元測試#
以下情況的測試不是單元測試:
- 連接資料庫
- 透過網路通訊
- 存取檔案系統
- 需要特殊環境設定才能執行
這些測試不一定不好,它們通常值得撰寫,也通常在單元測試框架中執行。但重要的是將它們與真正的單元測試分開,以維持一組可以快速執行的測試。
更高層級的測試#
單元測試很重要,但高層級測試(Higher-Level Tests)也有其價值。高層級測試可以用來驗證一組類別之間的協作行為,也常作為撰寫個別類別測試的起點。
測試覆蓋(Test Coverings)#
在 legacy 專案中開始做改變時,最安全的做法是先為要修改的程式碼加上測試。但這立刻帶來一個矛盾:
Legacy Code 的困境:修改程式碼前應該先有測試;但要加入測試,往往得先修改程式碼(打破相依性)。
相依性問題#
當類別直接依賴於難以在測試中使用的東西(如資料庫連線、Servlet、硬體裝置),就很難將它們放入測試框架中。解決這個問題的核心手段是打破相依性(Breaking Dependencies):
- 引入介面(Interface)來替代具體的實作
- 透過參數傳遞取代硬編碼的相依物件
- 使用 Primitivize Parameter 和 Extract Interface 等重構技術

Figure 2.1: Invoice update classes

Figure 2.2: Invoice update classes with dependencies broken
打破相依性以建立測試時,有時會讓程式碼看起來「更醜」。這就像手術的切口——表面上留了疤痕,但底下的一切都變得更健康了。日後當你能為這些區域加上更多測試時,連疤痕都可以癒合。
Legacy Code 變更演算法#
面對 legacy code 需要做變更時,可以遵循以下五個步驟:
- 辨識變更點(Identify Change Points)
- 找到測試點(Find Test Points)
- 打破相依性(Break Dependencies)
- 撰寫測試(Write Tests)
- 進行變更與重構(Make Changes and Refactor)
每次程式設計的目標不只是做出功能性變更,更要讓更多系統被測試覆蓋。隨著時間推移,被測試覆蓋的區域會像從海面升起的島嶼,逐漸擴大為穩固的大陸。
本書接下來的兩章將介紹 legacy code 工作中三個關鍵概念的背景知識:感測(Sensing)、分離(Separation) 和接縫(Seams)。