為什麼改一個東西要這麼久?#

在 legacy code 中,即使是最簡單的變更也可能花費很長時間。原因主要有兩個:

  1. 理解 (Understanding) – 搞清楚要改什麼就花了大半時間
  2. 延遲時間 (Lag Time) – 從做出變更到得到回饋之間的等待

Understanding(理解)#

隨著程式碼量的增長,理解的困難度會逐漸超越程式碼量本身。要花越來越多時間才能弄清楚應該改哪裡。

良好維護的系統 vs. Legacy 系統:

  • 良好維護的系統:弄清楚要改哪裡可能需要一些時間,但一旦了解,修改本身通常很簡單,而且你會對系統更有信心
  • Legacy 系統:不僅要花很長時間弄清楚該改什麼,修改本身也很困難,而且你可能覺得除了這次修改的狹窄範圍外,什麼也沒學到

將系統拆分成小的、命名良好、易於理解的部分,能讓工作更快。參考 Chapter 16 I Don’t Understand the Code Well Enough to Change It 和 Chapter 17 My Application Has No Structure


Lag Time(延遲時間)#

Lag time 是指從做出變更到收到回饋之間的時間。

作者用火星探測車 Spirit 做比喻:從地球發信號到火星需要 7 分鐘,操作控制需要等 14 分鐘才能知道結果。這聽起來荒謬,但許多軟體開發團隊的工作方式本質上類似 – 做一堆修改、開始 build、然後等待結果。

在大多數主流語言中,你總是可以用打破依賴的方式,讓你正在修改的程式碼在 10 秒以內完成重新編譯和測試。如果團隊真的有動力,甚至可以壓縮到 5 秒以內

快速回饋的影響#

當回饋循環壓縮到幾秒鐘:

  • 心智狀態從「規劃後等待」變成「即時嘗試」
  • 工作方式更像開車而非等公車
  • 注意力更集中,因為不用不斷等待下一次機會
  • 發現和修正錯誤所需的時間大幅縮短

Breaking Dependencies(打破依賴)#

依賴是阻礙快速開發的主要障礙。要讓 class 能獨立編譯和測試,必須打破不必要的依賴。

基本步驟#

  1. 嘗試在 test harness 中建立物件 – 遇到的問題通常就是需要打破的依賴
  2. 打破依賴後,檢查哪些東西仍然影響編譯時間
  3. 抽取 interface 來隔離 cluster 之間的依賴

Build Dependencies 範例#

假設有一組互相依賴的 class:

  • AddOpportunityFormHandler 依賴 ConsultantSchedulerDBAddOpportunityXMLGenerator
  • ConsultantSchedulerDB 建立 OpportunityItem

所有 class 都是 concrete(具體類別),互相緊密耦合。

Figure 7.1: Opportunity handling classes

步驟一:Extract Implementer

ConsultantSchedulerDB 使用 Extract Implementer,建立 interface:

<<interface>>
ConsultantSchedulerDB
        ^
        |
ConsultantSchedulerDBImpl  -->  OpportunityItem

現在 AddOpportunityFormHandler 依賴的是 interface。修改 ConsultantSchedulerDBImpl 時,AddOpportunityFormHandler 不需要重新編譯

Figure 7.2: Extracting an implementer on ConsultantSchedulerDB

步驟二:進一步隔離

OpportunityItem 也使用 Extract Implementer:

<<interface>>                    <<interface>>
ConsultantSchedulerDB           OpportunityItem
        ^                              ^
        |                              |
ConsultantSchedulerDBImpl  -->  OpportunityItemImpl

現在 AddOpportunityFormHandler 完全不依賴任何具體實作。

Figure 7.3: Extracting an implementer on OpportunityItem

步驟三:拆分 Package

OpportunityProcessing          DatabaseGateway
+ AddOpportunityFormHandler    + ConsultantSchedulerDB (interface)
+ AddOpportunityXMLGenerator   + OpportunityItem (interface)
                                        ^
                               DatabaseImplementation
                               + ConsultantSchedulerDBImpl
                               + OpportunityItemImpl

Figure 7.4: Refactored package structure

Dependency Inversion Principle(依賴反轉原則)

當你的程式碼依賴 interface 時,這個依賴通常非常輕微且不引人注意。你的程式碼不需要因為 interface 的實作改變而修改。依賴 interface 或 abstract class 比依賴具體 class 更好,因為你降低了特定變更觸發大規模重新編譯的機會。

測試也受益#

將測試放在對應的 package 中:

OpportunityProcessing
+ AddOpportunityFormHandler
+ AddOpportunityFormHandlerTest
- AddOpportunityXMLGenerator
- AddOpportunityXMLGeneratorTest

DatabaseImplementation
+ ConsultantSchedulerDBImpl
+ ConsultantSchedulerDBImplTest
+ OpportunityItemImpl
+ OpportunityItemImplTest

AddOpportunityFormHandler 使用 Extract InterfaceExtract Implementer,可以進一步讓其他 package 不受其修改影響。

Figure 7.5: Package structure

當你在設計中引入更多 interface 和 package 來打破依賴時,整體系統的 rebuild 時間會稍微增加(更多檔案需要編譯),但平均 build 時間會大幅下降,因為每次只需要重新編譯真正受影響的部分。


總結#

本章展示的技巧可以加速小群 class 的 build 時間,但這只是用 interface 和 package 管理依賴的冰山一角。打破依賴、將 class 拆分到不同 package 非常值得投資。一旦你有了可以獨立編譯和測試的小區域,你就能享受快速回饋帶來的所有好處。