問題背景#

許多功能的新增只是小修改——加一點程式碼、加幾個方法。最簡單的做法就是加在現有的 class 上。不幸的是,這種累積式的修改會讓 class 變成沼澤——方法和大量 class 讓人難以理解、難以變更、難以測試。

大 class 的三大問題:

  1. 混亂(Confusion)——50、60 個方法的 class,很難知道改了什麼會影響什麼
  2. 任務排程衝突(Task Scheduling)——20 個職責的 class 意味著多個開發者可能同時修改同一個 class
  3. 難以測試——大 class 是測試的噩夢

Single Responsibility Principle (SRP):每個 class 應該只有一個變更的理由。如果一個 class 有多個職責,它就有多個變更的理由。


Seeing Responsibilities#

識別職責是拆分大 class 的第一步,但也是最困難的部分。以下是作者提供的多種啟發式方法(heuristics)。

方法分群(Method Grouping)#

查看 class 的方法列表,尋找名稱或功能相似的方法群組。把相關的方法標記在一起——它們可能屬於同一個職責。

Figure 20.1: Rule parser

觀察隱藏的 Class#

如果 class 有大量 private method 或 instance variable,通常意味著裡面隱藏了一個(或多個)class。

Figure 20.2: Rule classes with responsibilities separated

Figure 20.3: RuleParser and TermTokenizer

找出變更的理由#

問自己:「這個 class 有幾種不同的變更原因?」每個原因可能對應一個獨立的職責。

Feature Sketches#

畫出 class 中 method 和 instance variable 之間的關聯圖。如果圖中出現明顯的叢集(clusters)——一組 method 使用一組 variable,另一組 method 使用另一組 variable——那這些叢集很可能就是不同的職責。

Figure 20.4: Variables in the Reservation class

Figure 20.5: extend uses duration

Figure 20.6: Feature sketch for Reservation

Figure 20.7: A cluster in Reservation

Figure 20.8: Reservation using a new class

Figure 20.9: Seeing Reservation in another way

Figure 20.10: Reservation using FeeCalculator

觀察 Interface Segregation#

如果 class 的不同使用者只使用其中一部分方法,這些方法群組可能代表不同的職責,可以透過抽取 interface 來分離。

Figure 20.11: The ScheduledJob class

Figure 20.12: ScheduledJob with extracted classes

Figure 20.13: A client-specific interface for ScheduledJob

Figure 20.14: Segregating the interface of ScheduledJob

看看 Scratch Refactoring 能告訴你什麼#

做一次 Scratch Refactoring——不在乎是否正確,只是嘗試移動方法和變數來獲得對結構的理解。完成後丟掉這次重構的結果。


Other Techniques#

關注目前的工作#

不需要一次看清所有職責。專注於你目前需要修改的部分,思考它是否代表一個可以分離的職責。

思考 Heuristic 的限制#

作者坦承每種 heuristic 都有局限性。沒有任何一種方法能保證正確識別所有職責。最好的做法是組合使用多種技巧,逐步深化理解。


Moving Forward#

Tactics:實作層級 vs. 介面層級的 SRP#

在大多數 legacy 系統中,最現實的起步是在實作層級(implementation level) 應用 SRP——從大 class 中抽取 class 並委派(delegate)。在介面層級(interface level)引入 SRP 需要更多工作,因為 client 也需要改變。

在實作層級引入 SRP 通常也讓後續在介面層級引入 SRP 變得更容易。

Extract Class 的步驟(無測試時)#

如果能取得測試,直接用 Martin Fowler 的 Extract Class 重構即可。如果無法取得測試,可以用以下保守的步驟:

  1. 識別要分離到另一個 class 的職責
  2. 找出是否有 instance variable 需要搬移,將它們移到 class 宣告中的獨立區塊
  3. 如果有完整的方法要搬移,抽取方法本體到新方法中,加上唯一的前綴(如 MOVING),並放在變數的獨立區塊旁邊。記得用 Preserve Signatures
  4. 如果方法的部分需要搬移,同樣抽取它們並加上 MOVING 前綴
  5. 對當前 class 和所有子類別做文字搜尋,確保要搬移的變數沒有在外部被使用。注意 variable shadowing 問題
  6. 將所有變數和方法直接搬移到新 class。在舊 class 中建立新 class 的實例,用 Lean on the Compiler 找到需要修改的地方
  7. 搬移完成且能編譯後,移除 MOVING 前綴

不搬移原始方法的原因是避免繼承相關的 bug。如果搬移的方法恰好 override 了基底 class 的方法,呼叫者會開始呼叫基底 class 的方法。透過抽取方法本體、保留原名、加前綴的方式,可以安全地避免這個問題。

繼承和 Shadowing 的陷阱#

  • 在 OO 語言中,derived class 可以宣告與 base class 同名的變數——這叫 shadowing
  • 如果你搬移了一個被 shadow 的變數,可能會無預警地改變行為
  • 解法:註解掉 shadowed variable 的宣告,讓 compiler 顯示所有使用處

After Extract Class#

從大 class 中抽取 class 通常是好的第一步。但實務上最大的危險是過度野心

你可能做了一次 Scratch Refactoring 或有了對理想結構的願景。但要記住:你的應用程式中的結構是可以運作的。它支撐了功能,只是可能不太適合繼續前進。有時候最好的做法是構思一個大 class 重構後的樣貌,然後先放著不管。你做這件事是為了發現可能性。向前推進時,要對現有的東西保持敏感,不一定要朝向理想設計前進,但至少要朝向更好的方向


總結#

拆分大 class 的最佳方法:

  1. 用各種 heuristic 識別職責
  2. 確保團隊其他人理解這些職責
  3. 需要的時候逐步拆分,而非一次性大重構
  4. 透過分散風險、配合日常工作逐步改善,讓系統結構隨時間進步