本章介紹兩種分散式模式:Remote FacadeData Transfer Object。它們是處理跨程序通訊時減少遠端呼叫次數、提升效能的核心策略。

Remote Facade#

在細粒度物件上提供粗粒度的 facade,以提升網路效率。

意圖#

Remote Facade 是一個粗粒度的 facade([Gang of Four]),覆蓋在一組細粒度物件之上。細粒度物件沒有遠端介面,Remote Facade 不包含任何領域邏輯——它只負責將粗粒度的方法呼叫轉譯為對底層細粒度物件的多次呼叫。

運作方式#

  • 在物件導向模型中,小物件和小方法提供良好的控制和替換能力,但代價是大量的物件間互動

  • 在單一位址空間內,細粒度互動效能良好;但遠端呼叫的成本比行程內呼叫高出數個數量級——即使兩個程序在同一台機器上

  • Remote Facade 將多個細粒度呼叫包裝成單一粗粒度呼叫:

    • 例如 address 物件有 getStreet()、getCity()、getZip() 等多個 getter,Remote Facade 提供一個 getAddressData() 一次取回所有資料
    • 類似地,setAddress(street, city, zip) 一次設定所有欄位
  • Facade 僅是薄殼——讀取方法從 setting method 讀取資料並呼叫細粒度物件的個別 accessor;所有驗證和計算邏輯仍留在細粒度物件中

    Figure 15.1: One call to facade causes several domain calls

  • 一個 Remote Facade 通常對應多個細粒度物件(如 CustomerService 包含 getAddressData、getPurchasingHistory、updateCreditData 等方法)

  • 粒度問題:作者偏好較少但較大的 Remote Facade(中型應用可能只有一個,大型應用可能半打),每個 facade 方法數量多但方法本身很小

  • Remote Facade 可以是 statefulstateless;stateless 可以 pooling 提升效能,stateful 需要搭配 session state 管理模式

  • 除了粗粒度介面,Remote Facade 也是套用安全控制交易控制的自然位置

Figure 15.2: Packages the remote interfaces

「Remote Facade 不包含領域邏輯。」作者強調這一點要重複三遍。任何 facade 都應該是只有最少職責的薄殼。若需要工作流或協調邏輯,應放在細粒度物件中或建立獨立的非遠端 Transaction Script。你應該能夠完全不使用 Remote Facade 就在本地端執行整個應用程式。

Remote Facade 與 Session Facade#

  • Session Facade(J2EE 社群常見)與 Remote Facade 在實務中有關鍵差異:Session Facade 通常包含領域邏輯(將多個 Transaction Script 放在遠端介面中),而 Remote Facade 不包含領域邏輯
  • 作者認為如果 Session Facade 包含領域邏輯,就不應該被稱為 facade

與 Service Layer 的關係#

  • Service Layer 不需要是遠端的,通常只有細粒度方法
  • 若 Domain Model 同時被本地和遠端使用,可以有 Service Layer + Remote Facade 兩層;若只被遠端使用,可以將 Service Layer 折入 Remote Facade
  • 若 Service Layer 不包含應用邏輯,可以直接讓 Remote Facade 成為 Service Layer

何時使用#

  • 適合:需要對細粒度物件模型進行遠端存取時(如展示層和 Domain Model 在不同程序中)
  • 不適合
    • 所有存取都在同一程序內(如 CGI script 和 Domain Model 在同一 Web 伺服器中),不需要此模式
    • Transaction Script 本身就是粗粒度的,通常也不需要 Remote Facade
  • Remote Facade 隱含同步式的遠端程序呼叫(RPC)風格;非同步、訊息式的通訊有許多優勢,但不在本書範圍內

Data Transfer Object#

在程序之間攜帶資料的物件,用於減少方法呼叫次數。

意圖#

Data Transfer Object(DTO) 是一個可序列化的物件,能夠在單一呼叫中攜帶多筆資料跨越網路。當使用遠端介面(如 Remote Facade)時,每次呼叫的成本都很高,因此需要在每次呼叫中傳輸更多資料。DTO 就是為此目的設計的資料容器。

Sun 社群中許多人使用「Value Object」這個名稱來指稱此模式,但作者使用 Value Object 表示不同的概念(見 p.486)。這是一個名稱衝突;作者遵循更通用的用法,稱之為 Data Transfer Object。

運作方式#

  • DTO 本質上只是一堆欄位加上 getter 和 setter,其價值在於能夠透過單一呼叫在網路上傳輸多筆資料
  • 一個 DTO 通常聚合來自多個伺服器端物件的資料——例如查詢訂單時,DTO 會包含訂單、客戶、明細項目、配送資訊等
  • 通常無法直接傳輸 Domain Model 物件,因為它們在複雜的關聯網絡中互相連接,難以序列化;也不希望將整個 Domain Model 複製到客戶端。因此需要從領域物件中提取簡化形式的資料
  • DTO 的欄位應該是基本型別、簡單類別(如字串和日期)或其他 DTO,結構應為簡單的圖形(通常是階層),而非 Domain Model 中複雜的圖結構
  • 設計原則:
    • 根據客戶端需求設計 DTO,通常對應到 Web 頁面或 GUI 畫面
    • 同一訂單可能有多個不同的 DTO,對應不同畫面
    • 可以為整個互動使用單一 DTO,也可以為不同的請求/回應使用不同的 DTO
    • 請求和回應可以共用同一個 DTO,也可以各自獨立
  • Record Set 是 DTO 的常見形式——SQL 查詢結果的表格式記錄集,Domain Model 可以產生 Record Set 傳給客戶端,客戶端視之如同直接來自 SQL。這種風格特別適合搭配 Table Module

序列化#

  • DTO 除了 getter/setter 外,通常還負責將自己序列化為可傳輸的格式
  • 二進位序列化:Java 和 .NET 都有內建支援,對簡單結構的 DTO 直接可用;但需注意兩端 DTO 類別版本同步——結構的任何變更都可能導致反序列化錯誤
  • XML 序列化:更容忍變更,易於閱讀和除錯,但需要更多頻寬且解析較慢
  • Map/Dictionary 序列化:用 Map 作為二進位序列化的載體,可提供一定的容錯性
  • 可使用反射(reflection)在 Layer Supertype 中實作通用的序列化/反序列化,避免每個 DTO 重複撰寫

從 Domain Object 組裝 DTO#

  • DTO 不應知道領域物件的存在(因為 DTO 需要部署在連線兩端),領域物件也不應依賴 DTO(因為 DTO 結構可能隨外部介面變更而改變)

    Figure 15.4: Assembler keeps domain model and DTOs independent

  • 因此使用獨立的 Assembler 物件負責在 DTO 與領域物件之間轉換(本質上是 Mapper 模式的應用)

  • Assembler 從領域物件建立 DTO(writeDTO),也從 DTO 更新或建立領域物件(updateModel / createModel)

  • 可以有多個 Assembler 共用同一個 DTO(不同情境有不同的更新語義)

  • DTO 容易自動產生,但 Assembler 通常更難自動化

Figure 15.5: Class diagram of artists and albums

Figure 15.6: Class diagram of data transfer objects

何時使用#

  • 適合:需要在兩個程序之間以單一方法呼叫傳輸多筆資料時
  • 替代方案
    • 使用多參數的 setting method 或 pass-by-reference 的 getting method(但 Java 等語言只允許回傳單一物件,限制較大)
    • 直接使用字串表示法(但耦合於表示格式,不利於變更)
  • 元件間通訊時特別值得建立 DTO,因為 XML DOM 不易操作,而 DTO 封裝了它
  • DTO 也常作為不同層之間的共用資料載體,每一層對 DTO 做修改後傳遞給下一層(Record Set 在 COM/.NET 中是很好的例子)
  • 非同步使用案例:同步時回傳 DTO,非同步時建立一個 Lazy Load 包裝的 DTO,使用者存取結果時才會阻塞等待