脈絡:用訊息整合應用程式時,訊息中的資料常源自各應用程式內部的領域物件。若用 Document Message,訊息本身可能直接代表一或多個領域物件;若用 Command Message,與命令相關的部分資料欄位也很可能取自領域物件。

訊息與物件之間有明顯差異:多數物件依賴以物件參考與繼承關係表現的關聯,而許多訊息基礎設施不支援這些概念——因為它們必須能與各式各樣的應用程式溝通,其中有些根本不是物件導向的。

為什麼不讓訊息長得跟領域物件一樣#

許多情況下我們根本無法控制訊息格式——它由 Canonical Data Model 或共同的訊息標準(例如 ebXML)所定義。

我們當然可以發布「對應領域物件」的訊息格式,再用訊息層中的 Message Translator 轉成共同格式(存取第三方系統的配接器常這麼做,因為那些系統不允許在應用程式內部做轉換)。

或者,領域直接以所需格式建立並發布訊息,不需要獨立的 Translator。

後者效能多半更好,因為不必發布中間訊息;而且若領域模型含有許多小物件,先把它們合併成單一訊息可以簡化路由、提升訊息層的效率

但這個作法的缺點是:領域必須知道訊息格式——若可能使用多種格式、或格式會改變,領域的維護就變得困難

為什麼不能把物件直接塞進訊息#

多數訊息基礎設施的 API 都有「Message」物件的概念,但它多半只能裝字串、數字、日期這類純量型別,不支援繼承或物件參考

假設我們送出一則含有物件參考的非同步訊息給某元件。要處理它,該元件就得解析那個參考——作法是向訊息來源索取該物件。但請求-回覆式的互動,違背了當初採用非同步訊息的部分動機(元件之間的鬆散耦合)

更糟的是:等訂閱者收到那則非同步訊息時,被參考的物件可能在來源系統中已經不存在了。

走訪相依樹?#

一種嘗試是走訪物件的相依樹,把所有相依物件都塞進訊息——例如一個 Order 物件參考五個 OrderItem,就把五個都放進去。

這確保接收端能取得根物件所參考的全部資料,但若使用細粒度的領域物件模型(許多物件彼此相關),訊息大小會迅速爆炸。我們會希望對「什麼要放進訊息、什麼不放」有更多控制

序列化成字串?#

就算領域物件自我完備、沒有任何參考,我們仍然無法把整個領域物件塞進訊息——多數訊息基礎設施為了語言獨立而不支援物件(JMS 的 ObjectMessage 與 .NET System.MessagingMessage 類別是例外,因為那些系統本身就綁定語言或平台)。

那把物件序列化成字串、存進一個叫 data 的字串欄位呢?

這也有一堆缺點:

  • Message Router 無法用物件屬性做路由,因為這個字串欄位對訊息層是「不透明」的
  • 測試與除錯變困難——我們得去解讀 data 欄位的內容
  • 無法依訊息型別路由——對基礎設施而言所有訊息長得一模一樣
  • 難以驗證訊息格式正確性——基礎設施不會驗證 data 欄位裡的任何東西
  • 無法使用語言執行期函式庫提供的序列化機制(那些表示法通常跨語言不相容),只好自己寫序列化程式碼

序列化成 XML?#

有些訊息基礎設施現在支援訊息中的 XML 欄位。

這緩解了部分缺點——訊息更容易解讀,某些訊息層還能直接存取 XML 字串中的元素

但我們現在得面對相當冗長的訊息與有限的資料型別驗證;而且仍然得寫「物件 ↔ XML」的轉換程式碼。依所用語言而定,這可能相當複雜——尤其若用的是不支援反射的老語言

為什麼對應程式碼該與領域物件分開#

  • 不想把「處理低階語言特性的程式碼」與應用邏輯混在一起

許多情況下會有一組程式設計師專責訊息層、另一組專注領域邏輯,把兩段程式碼塞進同一個物件,會讓兩隊很難平行工作

  • 把對應程式碼放進領域物件,會讓領域物件相依於訊息基礎設施(對應程式碼得呼叫訊息 API,例如實例化 Message 物件)。

這種相依多半是不受歡迎的——它讓領域物件無法在「不使用訊息」或「使用另一家廠商的訊息基礎設施」的情境中被重用,嚴重妨礙可重用性。

抽象層解決不了這件事#

我們常看到有人寫「抽象層」把訊息 API 包起來,讓處理訊息的程式碼獨立於訊息 API——這確實提供了一層間接,換掉廠商時只需重新實作抽象層。

對應程式碼該放在哪個類別?#

許多訊息由不只一個領域物件組成。既然無法透過訊息基礎設施傳遞物件參考,我們很可能得納入其他物件的欄位,有時甚至把整棵相依樹放進一則訊息。

那對應程式碼該放在哪個類別裡? 同一個物件可能屬於多種訊息型別、與不同物件組合——這個問題沒有簡單答案。

解法#

Messaging Mapper 存取一或多個領域物件,把它們轉換成訊息通道所要求的訊息;它也執行相反的功能——依進站訊息建立或更新領域物件

因為 Messaging Mapper 被實作成一個同時參考領域物件與訊息層的獨立類別兩層都不知道對方的存在——它們甚至不知道 Messaging Mapper 的存在。

圖 10-5:Messaging Mapper 解法示意

與相關模式的區別#

Messaging Mapper 是 [EAA] 中 Mapper 模式的特化,也與同書的 Data Mapper 有相通之處。任何做過物件-關聯(O-R)對應的人,都能理解在「使用不同典範的層」之間對應資料有多複雜——Messaging Mapper 內在的議題同樣複雜。

  • vs. 抽象層——在抽象層的情況下,領域物件不知道訊息 API,卻知道抽象層(抽象層本質上執行 Gateway 的功能);而在 Messaging Mapper 的情況下,物件完全不知道我們在處理訊息。
  • vs. Mediator [GoF]——兩者的意圖相似(分離元素),但Mediator 的情況下元素知道 Mediator 的存在,而 Messaging Mapper 的兩端都不知道。

那它是怎麼被喚起的?#

既然兩邊都不知道它的存在,Messaging Mapper 怎麼被呼叫?

多數情況下,Messaging Mapper 是被「訊息基礎設施或應用程式所觸發的事件」喚起的。既然兩者都不相依於它,事件通知可以透過一段獨立的程式碼完成,或把 Messaging Mapper 做成 Observer 模式

例如用 JMS API 時,可以實作 MessageListener 介面來接收進站訊息通知;同樣地,可以用 Observer 接收領域物件內部相關事件的通知並喚起 mapper。

若必須從應用程式直接喚起 Messaging Mapper,至少該定義一個 Messaging Mapper 介面,讓應用程式不相依於它的實作

減輕編碼負擔#

有些 Messaging Mapper 實作含有大量重複程式碼:從領域物件取一個欄位、存進訊息物件、下一個欄位、重複到做完。這很繁瑣,聞起來也很像程式碼重複。

有幾種工具能避免這種苦工:

  • 寫一個使用反射的通用 Messaging Mapper——走訪領域物件的所有欄位,存進訊息物件中同名的欄位。
  • 用可設定的程式碼產生器產生 mapper 程式碼——這在欄位命名上更有彈性(訊息欄位名與領域物件欄位名不必相同),也能設計聰明的辦法處理物件參考。
  • 利用框架內建的序列化——例如 Microsoft .NET 內建「物件 ↔ XML」序列化,省下大量苦工。

即使框架替我們做了部分「物件轉訊息」的工作,這種轉換也僅限於『語法層級』的轉譯。讓框架包辦一切很誘人,但它只會產生與領域物件一對一對應的訊息——而如前所述,這未必是我們要的,因為訊息的限制與設計準則,與領域物件的相當不同。