問題背景#
許多功能的新增只是小修改——加一點程式碼、加幾個方法。最簡單的做法就是加在現有的 class 上。不幸的是,這種累積式的修改會讓 class 變成沼澤——方法和大量 class 讓人難以理解、難以變更、難以測試。
大 class 的三大問題:
- 混亂(Confusion)——50、60 個方法的 class,很難知道改了什麼會影響什麼
- 任務排程衝突(Task Scheduling)——20 個職責的 class 意味著多個開發者可能同時修改同一個 class
- 難以測試——大 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 重構即可。如果無法取得測試,可以用以下保守的步驟:
- 識別要分離到另一個 class 的職責
- 找出是否有 instance variable 需要搬移,將它們移到 class 宣告中的獨立區塊
- 如果有完整的方法要搬移,抽取方法本體到新方法中,加上唯一的前綴(如
MOVING),並放在變數的獨立區塊旁邊。記得用 Preserve Signatures - 如果方法的部分需要搬移,同樣抽取它們並加上
MOVING前綴 - 對當前 class 和所有子類別做文字搜尋,確保要搬移的變數沒有在外部被使用。注意 variable shadowing 問題
- 將所有變數和方法直接搬移到新 class。在舊 class 中建立新 class 的實例,用 Lean on the Compiler 找到需要修改的地方
- 搬移完成且能編譯後,移除
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 的最佳方法:
- 用各種 heuristic 識別職責
- 確保團隊其他人理解這些職責
- 在需要的時候逐步拆分,而非一次性大重構
- 透過分散風險、配合日常工作逐步改善,讓系統結構隨時間進步