架構與大局思維#

Think About the Big Picture——系統內的決策必須與大局一致。

「大局」涵蓋兩層:

  • 系統層級:整體架構、商業目的、未來方向。
  • 環境層級:所屬部門或公司既有的框架、共用元件、整合慣例。

在重大設計決策前,問一句:「這個選擇能撐到下一輪需求?還是只解今天這個問題?」前者能避免再走一次架構手術。

兩個整體取捨:中央伺服器 vs. 對等架構#

當 Sam 要開第二家店,作者面對典型的架構取捨:

  • 中央伺服器:所有店共用 CDDiscCollectionRentalCollection,便於跨店報表與統一管理;但網路斷線時無法營運。
  • 對等架構(peer-to-peer):每店保有自己的集合,僅透過介面查詢另一家店。
interface StoreServiceProvider
    Boolean is_physical_id_in_cddisc_collection(PhysicalID physical_id)
    Boolean show_availability_of_CD_release(UPCCode upc_code)

最終選擇:

  • Sam 沒有跨店報表需求 → 中央伺服器的好處不顯著。
  • 對等架構建置成本低、抗網路風險高。
  • 若未來需求改變,仍可重新評估。

Figure 10-1. CDCatalogItemInStoreCollection sequence diagram

架構選擇不是「哪個比較好」,而是「在當前需求與資源下,哪個取捨最划算」。把選擇與理由寫進設計日誌,未來就有重新評估的依據。

把缺漏的概念提名為類別#

第二家店出現時,作者發現第一版漏掉了 Store 這個抽象:

class Store
    CommonString name
    Address address
    PhoneNumber phone_number

class StoreCollection
    Store where_is_physical_id_in_cddisc_collection(PhysicalID)
    Store[] show_availability_of_CD_release(UPCCode)
    Store[] find_all_stores()

這呼應 Clump Data So That There Is Less to Think About——把屬於同一概念的多個屬性群聚起來。

對外介面:別讓冷風吹進來#

當系統開始對外提供服務(Web Service、API),就需要一條額外的防線。

Don’t Let the Cold Air In——對外的介面必須做輸入驗證與紀錄。

實踐:

  • 所有外部輸入都應從原始字串轉成 SymbolFreeString 或對應 ADT,移除危險字元(;? 等)。
  • 用「tainted variable」概念:未驗證的輸入被視為汙染,驗證過後才標記為乾淨。
  • 對外端點要記錄 access log,便於後續分析合法用戶行為與惡意攻擊。

範例:對外服務 ExternalServicesProvider 與內部 StoreServiceProvider 分屬兩個介面,避免內部行為被直接暴露:

interface ExternalServicesProvider
    StoreExternalDTO[] show_availability_of_CD_release(UPCCode upc_code)

class StoreExternalDTO
    CommonString name
    Address address
    PhoneNumber phone_number

內部與外部使用不同 DTO,內部變動才不會無預警地影響合作夥伴的整合。

用既有輪子(Don’t Reinvent the Wheel)#

Sam 第三家店要開到 Canada 或 Mexico 時,要處理多幣別。作者把 Dollar 改名為 Money

enumeration CurrencyID {USD, CAD, MXN}

class Money
    CurrencyID id
    Money(CurrencyID id)
    // 同 Dollar 的操作與屬性

再把 ISO 4217 引入並重用 Java 內建 CurrencyCurrencyFormat

Don’t Reinvent the Wheel——找既存解再說,標準與現成元件能省下無數細節。

Don’t Overclassify——貨幣只在資料上不同,行為相同 → 一個類別 + enum 即可,不必為每個貨幣寫類別。

避免過早通用化#

Avoid Premature Generalization——先解決具體問題再通用化。

理由:

  • 一開始就追求「通用支付」會被無數匯率、稅務、結算規則拖住。
  • 在 Sam 的場景把單一幣別解透,累積經驗後再泛化,反而能設計出貼合實務的 Money
  • Brad Appleton 提醒:即使有通用化的點子,也不必馬上做;至少要避免在當下做決定時,把未來的彈性堵死。

真實案例:列印伺服器#

Chapter 15 的列印伺服器案例展示在現實系統落地預重構:

  • 每個元件都圍繞單一抽象設計(PrintJob、ReleaseStation、PaymentMethod)。
  • 透過 message-based 通訊解耦:客戶端送 request、伺服器回應 reply,每種訊息對應一個類別。
  • 在時程壓力下(10 週交差貿易展示),作者沒有把所有 Extreme Abstraction、Extreme Separation 的指南都套滿,但仍把「介面契約」與「最小可動」做好。

Figure 15-1. Sequence diagram for handling a message

在時間限制下,預重構的價值在於先想清楚介面與責任分配,內部實作可以接受不完美。

真實案例:反垃圾郵件#

Chapter 16 的反垃圾郵件案例強調職責分離:

  • ReceivingMailServer:收信並把信件交給檢查器。
  • ReceivedMailExaminer:負責跑垃圾郵件規則並標記。
  • MailReport:把檢查結果以使用者偏好的形式(附在訊息末、加在 header、忽略)呈現。

反垃圾郵件本質就是 Pipeline + Strategy 的組合。每段管線只做一件事,誰要更新規則就只動 ReceivedMailExaminer 的策略物件。

Figure 16-1. Process of delivering email

Figure 16-2. State transition for SMTP

從具體到抽象:把 Sam 的系統泛化#

第十四章末尾總結:可以把 Sam 系統泛化為「租借任意可識別的物件」:

  • CDDiscRentableItem
  • CDReleaseCatalogItem
  • RentalOperationsCatalogOperations 介面分離仍然適用。
  • 特定領域方法(如「依演出者搜尋」)對通用版不適用,可作為衍生介面。

在具體場景中累積足夠經驗後,再來做泛化會更踏實——這是「先解具體再通用」的另一個示範。

架構與設計指南總結#

指南訊息
Think About the Big Picture設計決策要與大局一致
Don’t Let the Cold Air In對外介面驗證與紀錄
Don’t Reinvent the Wheel用既有元件
Don’t Overclassify用 enum 取代為每個變化建類別
Avoid Premature Generalization先解具體再通用
Clump Data So That There Is Less to Think About缺漏的抽象要立刻提名
Decouple with Associations跨概念互動要靠關聯類別解耦