為什麼要極致分離#
關注點分離(separation of concerns)的目的,是讓每個類別、每個方法只面對一個會變動的軸線。當需求變動時,可以只改一個地方而不是牽動整個系統。
套用分離原則通常會增加類別與方法數量。看似囉嗦,實際上是把「未來變動成本」攤提到「現在的設計」中。
預重構態度與 DRY#
Adapt a Prefactoring Attitude——在重複發生之前消除重複。Don’t Repeat Yourself(DRY)是它的更廣義版本:每一份知識都該有單一、明確、權威的表述。
實踐方式:
- 複製貼上前停一秒:要複製的程式片段,往往該被抽成方法或共用工具。
- 註解寫的是 how 而非 what:當你忍不住對某段程式寫註解解釋如何運作時,那段就應該是獨立方法。
- 相同骨架的類別用模板:例如約定每個類別都要有
to_string()/from_string(),就建一個原始碼或介面模板,由 IDE 自動產生骨架,避免手抄。 - 單一資料源頭,自動衍生其他形式:例如以 XML 描述資料表,由轉換程式產生 SQL 與各語言的存取類別,改動一處即可同步全域。
類別格言:高內聚、低耦合#
高內聚(Cohesion)#
- 每個類別只代表一個抽象概念。
- 寫得出一句話的類別說明就算過關;寫不出來表示混了多個抽象。
- 範例:
Time一旦同時代表「時鐘時間」與「時間區間」,加減秒數就會出現怪結果——把概念寫清楚就能避免。
低耦合(Coupling)#
耦合分三級:
- 緊耦合(tight):依賴對方實作。
- 共同耦合(common):透過介面方法溝通,實作改了不必跟著改。
- 鬆耦合(loose):只仰賴對方存在,不仰賴介面。
Honor the Class Maxims——讓類別保持高內聚、低耦合。在多數情況下追求 common 或 loose,遵循「Design to an interface, not an implementation」。
方法該不該屬於這個類別#
Place Methods in Classes Based on What They Need——若方法不需要實例資料,就不該屬於該類別;若它只需要實例資料,就該屬於該類別。
判斷實例:
break_into_lines(text, charsPerLine)不操作CDRelease的任何屬性 → 應放到StringHelper工具類別,或讓MyString繼承String並加上方法。- 抉擇取捨:工具類別保留純函式氣味,繼承類別則讓物件能「自己處理自己」,但容易被塞滿不相關方法。
一件事做好就好(One Little Job)#
借用 Unix pipe 的精神:每個程式做一件小事,靠組合解決大問題。
- 類別只代表一個概念,方法只做一件小工作。
File只做基本讀寫;遇到「按行讀」就用FileWithLines委派給File,避免把File塞滿不相關功能。- 高層類別建立在低層之上,低層更通用、更常被重用。
Do a Little Job Well and You May Be Called Upon Often——專注做小事的方法與類別,更容易被重複利用。
政策(What)與實作(How)的分離#
if (is_overdue())
process_rental_which_ended_late();vs.
if (today > end_date)
process_rental_which_ended_late();兩者比較:
- 前者的
is_overdue()把「逾期」的定義集中於一處,可被多個 use case 重用,避免不同 use case 對逾期的判斷不一致。 - 後者把計算邏輯散到呼叫端,下一個團隊成員可能會寫成
today > start_date.add(base_rental_period),造成漂移。
Separate Policy from Implementation——把「做什麼」與「怎麼做」分開,what 的部分才會清楚、好維護。
寫程式時可先寫意圖:
if (a_customer.is_good_customer())
a_customer.provide_discount();再分別實作 is_good_customer() 與 provide_discount()。
用關聯類別解耦兩個概念#
當 Sam 要求「列出某客戶目前所有租借」「保留所有歷史租借」時,原本耦合在 CDDisc 上的 Rental 設計就不夠用。
- 原設計:
CDDisc→Rental→Customer,租借結束後關聯就消失。 - 改善:把
Rental提升為關聯類別(association class),同時參照CDDisc與Customer,並由RentalCollection集中管理:
class Rental
Customer renter
CDDisc cd_disc
Timestamp start_time
Timestamp end_time
Dollar rental_fee
Days base_rental_period
class RentalCollection
RentalContractDTO start_rental(CDDisc, Customer)
end_rental_of_cddisc(CDDisc)
Rental[] retrieve_current_rentals_for_customer(Customer)
Rental[] retrieve_all_rentals_for_customer(Customer)
Customer[] retrieve_customers_who_rented_a_cd_disc(CDDisc)
Rental retrieve_current_rental_for_cd_disc(CDDisc)如此 CDDisc 不再耦合 Customer,雙方都對 Rental 對等存在,歷史租借也能保留。

Figure 9-1. Rental association classes
Decouple with Associations——關聯類別解耦兩個被關聯的類別。
切分介面(Split Interfaces)#
當不同的客戶端只用到介面的不同部分,就把介面拆開:
RentalOperations原本同時包含「租借操作」與「狀態查詢」。- 拆出
StatusOperations(包含is_cd_disc_rented()),讓CatalogOperations只依賴狀態而不能執行租借操作。 - 拆分後權限控管也跟著乾淨:客戶端只能存取自己需要的介面。

Figure 10-2. Split interfaces
Split Interfaces——當多個客戶使用單一介面的不同片段時,把介面切成多個。
Proxy 與 Adopt-and-Adapt:用組合擴充行為#
需要新增非核心職責時(log、計次、安全檢查),不要直接改實作,而是套上 Proxy。
範例:Sam 想驗算第三方 ZIP code 服務的呼叫次數。
ZipCodeVerificationTracker implements ZipCodeVerificationService {
ZipCodeVerificationService next_implementation
= new ZipCodeVerificationImplementation();
ZipCode find_zip_code_for_address(Address address) {
track_this_call();
return next_implementation.find_zip_code_for_address(address);
}
}- 透過 Proxy 串接,把「追蹤呼叫」與「實作」分離。
ZipCodeCorrectionService對切換實作毫無感知,只要替換一行注入。

Figure 11-1. Proxy pattern for ZipCodeVerificationService
延伸概念:
- Adopt and Adapt:先設計理想介面,再把實作(包含第三方)改寫成符合介面的形狀。
- Do a Little and Pass the Buck:用 Proxy 鏈每一層只做一件事,再交棒給下一層。
- When in Doubt, Indirect:用工廠(Factory)或設定取得實例,避免呼叫端直接 new 出實作;組態驅動的選擇可讓部署時切換。
- Dependency Injection:把依賴從工廠移到呼叫端注入,把「依賴的決策」與「物件的建立」進一步分離。
Adopt and Adapt、Do a Little and Pass the Buck、When in Doubt, Indirect——這三條相輔相成,建立可替換、可組合的元件。
商業規則自成一格#
商業規則(discount、發票、逾期費政策等)會頻繁變動,且是業主面向的語言。
compute_discount()可以放在Customer類別(因為跟客戶相關),也可以放到專門的BusinessRules類別,便於業主集中審查。- 規則複雜時,可採用商業規則語言(ILOG JRules、Mandarax、BRML)將規則外部化,與程式邏輯解耦。
Business Rules Are a Business unto Themselves——商業規則是獨立的存在,不要跟其他邏輯纏在一起。
切分的取捨#
過度切分會讓導覽變難,新增最簡單的功能也得跨多個類別。要保留判斷力:當切分理由(變動軸、權限、可測性、重用)成立時才切;單純為「比較乾淨」而切會增加維護成本。
分離原則總結#
| 指南 | 訊息 |
|---|---|
| Adapt a Prefactoring Attitude | 重複發生之前先消除 |
| Don’t Repeat Yourself (DRY) | 每份知識只在一處表達 |
| Honor the Class Maxims | 高內聚、低耦合 |
| Place Methods in Classes Based on What They Need | 看資料需求決定方法歸屬 |
| Do a Little Job Well | 一件事做好,重用機會更多 |
| Separate Policy from Implementation | what 與 how 拆開 |
| Decouple with Associations | 用關聯類別把兩個概念解耦 |
| Split Interfaces | 不同客戶用不同片段就拆開介面 |
| Do a Little and Pass the Buck | 用 Proxy 鏈分擔職責 |
| When in Doubt, Indirect | 不確定就加一層間接 |
| Business Rules Are a Business unto Themselves | 商業規則獨立成一塊 |