脈絡:用訊息整合應用程式時,訊息中的資料常源自各應用程式內部的領域物件。若用 Document Message,訊息本身可能直接代表一或多個領域物件;若用 Command Message,與命令相關的部分資料欄位也很可能取自領域物件。
但訊息與物件之間有明顯差異:多數物件依賴以物件參考與繼承關係表現的關聯,而許多訊息基礎設施不支援這些概念——因為它們必須能與各式各樣的應用程式溝通,其中有些根本不是物件導向的。
為什麼不讓訊息長得跟領域物件一樣#
許多情況下我們根本無法控制訊息格式——它由 Canonical Data Model 或共同的訊息標準(例如 ebXML)所定義。
我們當然可以發布「對應領域物件」的訊息格式,再用訊息層中的 Message Translator 轉成共同格式(存取第三方系統的配接器常這麼做,因為那些系統不允許在應用程式內部做轉換)。
或者,領域直接以所需格式建立並發布訊息,不需要獨立的 Translator。
後者效能多半更好,因為不必發布中間訊息;而且若領域模型含有許多小物件,先把它們合併成單一訊息可以簡化路由、提升訊息層的效率。
但這個作法的缺點是:領域必須知道訊息格式——若可能使用多種格式、或格式會改變,領域的維護就變得困難。
為什麼不能把物件直接塞進訊息#
多數訊息基礎設施的 API 都有「Message」物件的概念,但它多半只能裝字串、數字、日期這類純量型別,不支援繼承或物件參考。
假設我們送出一則含有物件參考的非同步訊息給某元件。要處理它,該元件就得解析那個參考——作法是向訊息來源索取該物件。但請求-回覆式的互動,違背了當初採用非同步訊息的部分動機(元件之間的鬆散耦合)。
更糟的是:等訂閱者收到那則非同步訊息時,被參考的物件可能在來源系統中已經不存在了。
走訪相依樹?#
一種嘗試是走訪物件的相依樹,把所有相依物件都塞進訊息——例如一個 Order 物件參考五個 OrderItem,就把五個都放進去。
這確保接收端能取得根物件所參考的全部資料,但若使用細粒度的領域物件模型(許多物件彼此相關),訊息大小會迅速爆炸。我們會希望對「什麼要放進訊息、什麼不放」有更多控制。
序列化成字串?#
就算領域物件自我完備、沒有任何參考,我們仍然無法把整個領域物件塞進訊息——多數訊息基礎設施為了語言獨立而不支援物件(JMS 的 ObjectMessage 與 .NET System.Messaging 的 Message 類別是例外,因為那些系統本身就綁定語言或平台)。
那把物件序列化成字串、存進一個叫 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」序列化,省下大量苦工。
即使框架替我們做了部分「物件轉訊息」的工作,這種轉換也僅限於『語法層級』的轉譯。讓框架包辦一切很誘人,但它只會產生與領域物件一對一對應的訊息——而如前所述,這未必是我們要的,因為訊息的限制與設計準則,與領域物件的相當不同。