為什麼要極致分離#

關注點分離(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 設計就不夠用。

  • 原設計:CDDiscRentalCustomer,租借結束後關聯就消失。
  • 改善:把 Rental 提升為關聯類別(association class),同時參照 CDDiscCustomer,並由 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 Implementationwhat 與 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商業規則獨立成一塊