本章聚焦於構成領域模型的個別元素,探討如何在不犧牲 Model-Driven Design 精髓的前提下,將模型忠實地映射到軟體實作中。主要涵蓋四個核心建構單元:Association(關聯)、Entity(實體)、Value Object(值物件)、Service(服務),以及組織這些元素的 Module(模組)。
Association#
模型中物件之間的關聯看似容易繪製,但實作起來卻是潛在的泥沼。每一條可遍歷的關聯,在軟體中都必須有對應的機制來實現。
簡化關聯的三種策略#
- 施加遍歷方向(Imposing a traversal direction)——將雙向關聯縮減為單向
- 加入限定詞(Adding a qualifier)——有效降低多重性
- 移除非必要的關聯(Eliminating nonessential associations)
遍歷方向反映領域本質#
以「國家」與「總統」為例:我們通常從國家查詢其歷任總統,很少從「George Washington」反查他是哪國總統。將雙向關聯縮減為從國家到總統的單向關聯,不僅是實作上的便利,更反映了領域中方向性的自然偏好——保持 Person 類別獨立於 President 這個相對不基本的概念。

Figure 5.1: Some traversal directions reflect a natural bias in the domain.
用限定詞降低多重性#
進一步探究會發現:一個國家在同一時間通常只有一位總統。加入「時間」作為限定詞,便將一對多關聯縮減為一對一,同時將重要的業務規則嵌入模型之中。

Figure 5.2: Constrained associations communicate more knowledge and are more practical designs.
範例:Brokerage Account#
書中以券商帳戶為例,說明關聯可以用不同方式實作(物件集合 vs. 資料庫查詢),只要行為與模型一致即可。當在 Brokerage Account 與 Investment 之間加入 stock symbol 作為限定詞,多對多變成了透過鍵值查詢的一對一,使模型更精確、實作更容易維護。
持續約束關聯以反映領域偏好,不僅讓關聯更具表達力、更容易實作,也賦予剩下的雙向關聯更強的語義意義——保留雙向代表雙向性本身就是領域的特徵。
Entity(又稱 Reference Object)#
許多物件的根本定義不在於其屬性,而在於一條貫穿生命週期的連續性與身份(continuity and identity)。
什麼是 Entity#
- 一個物件如果主要由其**身份(identity)**而非屬性來區分,就稱為 Entity
- Entity 擁有可能劇烈變化的生命週期,但必須維持一條身份的連續線索
- 它可以是人、城市、汽車、彩券,或銀行交易——任何在應用程式中需要跨越狀態變化被追蹤的事物
物件導向語言內建的 identity 操作(如 Java 的
==)只比較記憶體位址,對領域層面的身份識別毫無意義。從資料庫重新載入或跨網路傳輸後,原始的記憶體身份就會消失。Entity 的身份定義必須來自領域模型本身。
同一事物在不同情境下可能是或不是 Entity#
書中以「座位」為例:
- 對號入座的場館——每個座位有座位號,是 Entity
- 自由入座的活動——個別座位不需區分,只關心總數,座位就不是 Entity
這說明身份不是事物的固有屬性,而是因為「有用」而被賦予的意義。
建模 Entity 的原則#
- 精簡為上:將 Entity 的定義剝離到最本質的特徵,特別是用來識別或匹配它的屬性
- 只加入概念上不可或缺的行為與屬性
- 其餘行為和屬性應移出到其他關聯的物件(可能是其他 Entity 或 Value Object)
- Entity 通常透過協調其擁有的物件來履行職責

Figure 5.5: Attributes associated with identity stay with the ENTITY.
設計 Identity 操作#
建立 Entity 的身份識別方式有幾種策略:
| 策略 | 說明 | 範例 |
|---|---|---|
| 自然屬性組合 | 利用屬性或屬性組合保證唯一 | 報紙名稱 + 城市 + 日期 |
| 系統產生的 ID | 附加一個不可變的唯一符號 | 自動遞增編號、UUID |
| 外部機構發放的 ID | 使用外部權威機構的識別碼 | 身份證字號、社會安全號碼 |
| 使用者指定的 ID | 由使用者負責確保唯一性 | 常客飛行號碼 |
無論採用哪種策略,根本的概念問題始終是:兩個物件代表「同一件事」究竟意味著什麼? 如果 ID 或比較操作不對應領域中有意義的區分,只會製造更多混亂。這也是為什麼身份指派操作經常需要人為介入——例如支票簿對帳軟體可能提供可能的匹配,但最終判定仍由使用者做出。
Value Object#
許多物件沒有概念上的身份,它們描述的是某件事物的特徵。
什麼是 Value Object#
- 代表領域中描述性面向、沒有概念身份的物件
- 我們只關心它是什麼(what),而不是它是哪一個(which)
- 常見的例子:顏色、字串、數字、地址(在某些情境下)、金額
給所有物件都賦予身份會帶來三重代價:(1)系統必須追蹤所有身份,犧牲效能;(2)需要額外分析工作定義有意義的身份;(3)讓所有物件看起來一模一樣,模糊了模型。
Address 是 Entity 還是 Value Object?#
這取決於誰在問:
- 郵購公司——Address 用於確認信用卡和寄送包裹,不需區分同一地址的不同訂單 → Value Object
- 郵政服務——Address 是遞送路線階層的一部分,會因郵遞區號重劃而連動 → Entity
- 電力公司——Address 對應服務線路的目的地,需識別同一地址的不同申請 → Entity(或者用 Dwelling 作為 Entity,Address 作為其 Value Object 屬性)
Value Object 的設計原則#
- 視為不可變(immutable)——變更管理簡化為完全替換
- 屬性應構成概念上的整體——例如街道、城市、郵遞區號不應是 Person 的獨立屬性,而應組合成一個完整的 Address Value Object

Figure 5.6: A VALUE OBJECT can give information about an ENTITY. It should be conceptually whole.
複製 vs. 共享#
Value Object 不關心是哪個實例,這給予設計上的自由度:
- 複製:每個擁有者持有自己的副本,簡單安全
- 共享:多個物件指向同一實例,節省空間(FLYWEIGHT 模式),但前提是物件必須不可變
適合共享的情境:
- 資料庫中節省空間或物件數量至關重要
- 通訊開銷低(如集中式伺服器)
- 共享物件嚴格不可變
何時允許可變#
雖然不可變是首選,但在以下情況可考慮允許 Value Object 可變:
- 值頻繁變更
- 物件建立或刪除成本高
- 替換(而非修改)會打亂 clustering
- 幾乎不共享
如果 Value Object 的實作是可變的,就絕不能共享。無論是否共享,盡可能將 Value Object 設計為不可變。
涉及 Value Object 的關聯#
- Value Object 之間的雙向關聯沒有意義——沒有身份,說「一個物件指回同一個指向它的 Value Object」是無意義的
- 如果發現兩個 Value Object 之間似乎需要雙向關聯,應重新考慮它是否其實擁有尚未被明確識別的身份
flowchart TD
Start{物件需要被追蹤身分嗎?}
Start -->|是| Entity[Entity]
Start -->|否,只關心屬性| VO{屬性會改變嗎?}
VO -->|不可變| ValueObject[Value Object]
VO -->|需要變更| VOmutable[考慮替換而非修改]
Entity --> IDStrategy{Identity 策略}
IDStrategy --> Auto[系統自動產生 ID]
IDStrategy --> Natural[領域自然鍵]
IDStrategy --> External[外部指派]
style Start fill:#e0e0e0,stroke:#333,stroke-width:2px,color:#000
style Entity fill:#f9c74f,stroke:#f77f00,stroke-width:2px,color:#000
style ValueObject fill:#a8dadc,stroke:#457b9d,stroke-width:2px,color:#000
style VOmutable fill:#a8dadc,stroke:#457b9d,stroke-width:2px,color:#000
style IDStrategy fill:#f9c74f,stroke:#f77f00,stroke-width:2px,color:#000
note["同一概念在不同 Context 可能不同分類\n(如 Address 可以是 Entity 或 Value Object)"]
style note fill:#fff3cd,stroke:#856404,stroke-width:2px,color:#000Service#
有時候,領域中的某些操作就是不適合歸屬於任何物件。
為什麼需要 Service#
- 某些重要的領域操作在 Entity 或 Value Object 中找不到自然的歸屬
- 強行把操作塞進不合適的物件會使該物件失去概念清晰度,難以理解和重構
- 複雜操作可能淹沒簡單物件,模糊其角色
Service 的三個特徵#
- 操作涉及的領域概念不自然屬於任何 Entity 或 Value Object
- 介面以領域模型中的其他元素來定義
- 操作是無狀態的(stateless)
Service 的命名傾向於使用動詞而非名詞,強調的是「它能為客戶端做什麼」。操作名稱應來自 Ubiquitous Language,參數和結果應為領域物件。
Service 的分層#
Service 存在於不同層中,區分它們的歸屬非常重要:
| 層級 | 範例:資金轉帳 | 職責 |
|---|---|---|
| Application | Funds Transfer App Service | 解析輸入(如 XML 請求)、發送訊息給 Domain Service、監聽確認、決定是否透過 Infrastructure Service 發送通知 |
| Domain | Funds Transfer Domain Service | 與 Account、Ledger 物件互動,執行借貸操作,回傳結果(轉帳是否成功) |
| Infrastructure | Send Notification Service | 依指示發送電子郵件、信件等通訊 |
sequenceDiagram
participant App as Application Service
participant Domain as Domain Service
participant Infra as Infrastructure Service
App->>Domain: 轉帳請求(借方、貸方、金額)
Domain->>Domain: 借方 Account.debit()
Domain->>Domain: 貸方 Account.credit()
Domain->>Infra: 發送通知
Infra-->>Domain: 通知已發送
Domain-->>App: 轉帳完成粒度(Granularity)#
- 中等粒度的無狀態 Service 更容易在大型系統中重複使用,因為它們在簡單介面後封裝了重要功能
- 過於細粒度的領域物件在分散式系統中會導致低效的訊息傳遞
- 適當引入 Domain Service 有助於維持層與層之間的清晰界線,防止領域知識洩漏到應用層
Service 的存取方式#
- 不一定需要分散式架構(如 J2EE、CORBA)來發布 Service
- 一個簡單的 SINGLETON 就足以提供存取
- 應根據實際需求選擇架構,而非為了「以防萬一」的彈性而過度工程化
Module(又稱 Package)#
Module 是老牌的設計元素,其首要動機是降低認知負荷。Module 給予人們兩種觀看模型的視角:深入某個 Module 的細節,或鳥瞰 Module 之間的關係。
核心原則#
- 低耦合、高內聚不只是技術度量,更是概念層面的原則
- 低耦合:人一次能思考的事物有限
- 高內聚:片段不連貫的想法與一鍋大雜燴同樣難以理解
- Module 是溝通機制——將某些類別放在同一個 Module,等於告訴下一位開發者「請把它們一起思考」
- Module 的名稱應進入 Ubiquitous Language
設計指引#
選擇能講述系統故事並包含一組內聚概念的 Module。如果模型是在講故事,Module 就是章節。
- Module 應與模型共同演進,而非停留在早期形態
- 重構 Module 比重構類別的代價更高,但不應因此避免
- Module 名稱應反映對領域的深入理解
基礎設施驅動的封裝陷阱#
書中以 J2EE 為例,說明框架將單一領域物件拆分到多個 tier(entity bean、session bean)並分置不同 package 的做法,會帶來兩個嚴重成本:
- 程式碼不再顯露模型——實作概念物件的元素被拆散
- 心智分割空間耗盡——框架用掉了所有的分區配額,領域開發者失去按領域概念分組的能力
除非真的打算將程式碼部署到不同伺服器,否則應將實作同一概念物件的所有程式碼放在同一個 Module(甚至同一個物件)中。用封裝來分離領域層與其他程式碼,其餘的封裝自由度留給領域開發者。
Agile Module#
- Module 需要與模型和程式碼一起重構
- 早期 Module 選擇的錯誤會導致高耦合,使後續重構更困難,形成慣性
- 克服方法:鼓起勇氣,根據實際遇到的問題點重新組織 Module
Modeling Paradigm#
Model-Driven Design 需要與建模典範(modeling paradigm)配合的實作技術。目前物件導向是主流典範。
物件導向為何主導#
- 簡單與複雜的平衡:概念直覺易懂,非技術成員也能跟上 UML 圖
- 成熟的基礎設施:經過多年發展,常見需求都有現成方案
- 龐大的開發者社群:模式和最佳實踐已廣泛傳播
在物件世界中融入非物件元素#
當領域的某些部分用其他典範(如規則引擎、工作流引擎)更自然表達時,可以混合使用,但必須注意:
混合典範的四條經驗法則:
- 不要對抗實作典範——為領域找到適合該典範的模型概念
- 依靠 Ubiquitous Language——即使工具之間沒有嚴格連接,一致的語言也能防止設計分歧
- 不要執著於 UML——有時其他繪圖風格或簡單的英文描述更適合
- 保持懷疑——工具真的值得它帶來的複雜度嗎?規則可以用物件表達,不一定需要規則引擎
在承擔混合典範的負擔之前,應先窮盡主流典範內的選項。即使某些領域概念不是顯而易見的物件,通常仍可在物件典範中建模(第 9 章將討論用物件技術建模非傳統概念)。