本章探討如何將經典的設計模式(Design Patterns)運用在領域模型中,使其不僅是技術解決方案,更能表達領域概念。Evans 以 Strategy (Policy)Composite 兩個模式為核心範例,說明當設計模式與領域概念吻合時,能同時帶來技術上的彈性與模型上的清晰度。

將設計模式應用於領域層時,首要關注的不是技術便利性,而是該模式是否真正契合領域概念。模式必須能表達概念性的領域意涵,而非僅僅是技術問題的技術解法。


Strategy(又名 Policy)#

GoF 定義#

Define a family of algorithms, encapsulate each one, and make them interchangeable. STRATEGY lets the algorithm vary independently from clients that use it. [Gamma et al. 1995]

領域模型中的動機#

領域模型經常包含非技術驅動、而是在問題領域中有實際意義的流程(process)。當這些流程存在多種合法的替代方案時,問題就會浮現:

  • 選擇適當流程的複雜度與多個流程本身的複雜度交織在一起
  • 我們開始描述各種選項時,流程的定義變得笨拙且複雜
  • 實際的行為替代方案被混入其餘行為中而變得模糊

我們希望將變化的部分從流程的主要概念中分離出來,這樣就能更清楚地看到主流程與各選項。STRATEGY 模式正好解決這個問題——雖然它在軟體設計社群中的焦點是技術性的,但在此被作為模型中的概念來應用。

核心做法#

Factor the varying part of a process into a separate “strategy” object in the model. Factor apart a rule and the behavior it governs. Implement the rule or substitutable process following the STRATEGY design pattern. Multiple versions of the strategy object represent different ways the process can be done.

  • 將流程中變化的部分抽出為模型中獨立的 Strategy 物件
  • 規則與其所治理的行為分離
  • 多個版本的 Strategy 物件代表流程的不同執行方式

傳統觀點將 STRATEGY 視為「可替換不同演算法」的設計模式,但在領域模式的用法中,重點在於它表達概念的能力——通常是一個流程(process)或一條政策規則(policy rule)。

範例:Route-Finding Policies#

一個 Route Specification 被傳入 Routing Service,該 SERVICE 的工作是建構一個滿足 SPECIFICATION 的詳細 Itinerary。這個 SERVICE 是一個最佳化引擎,可以調整為尋找最快的路線最便宜的路線

Figure 12.1: A SERVICE interface with options will need conditional logic.

這種設計看似合理,但深入檢視 routing 程式碼會發現:

  • 條件判斷散布在每個計算中,fastest 或 cheapest 的決策到處出現
  • 當新增更細微的路線選擇標準時,問題會更加嚴重

解決方法是將那些調整參數分離成 STRATEGIES,明確地表達它們,並作為參數傳入 Routing Service。

Figure 12.2: Options determined by choice of STRATEGY (POLICY) passed as argument

Routing Service 現在以統一且無條件的方式處理所有請求,尋找由 Leg Magnitude Policy 計算出的低量級 Leg 序列。

設計帶來的好處#

  • 行為可控制與擴展:安裝適當的 Leg Magnitude Policy 即可控制 Routing Service 的行為
  • 圖示中的 fastest 或 cheapest 只是最明顯的策略,還可能有:
    • 平衡速度與成本的組合策略
    • 偏好使用公司自有運輸而非外包的策略
  • 解耦使邏輯清晰且易於測試:若不使用 STRATEGY,這些邏輯會纏繞在 Routing Service 內部並膨脹其介面
  • 領域規則變得明確且獨立:選擇 Leg 的根本規則——基於個別 Leg 的某個屬性(可能是衍生的)濃縮為單一數值——現在是顯式的

這使得用領域語言做出簡潔陳述成為可能:

The Routing Service chooses an Itinerary with a minimum total magnitude of the Legs based on the chosen STRATEGY.

此處的討論暗示 Routing Service 在搜尋 Itinerary 時會逐一評估 Legs。這種做法概念上直接,可作為合理的原型實作,但效能可能無法接受。同一個介面將在第 14 章以完全不同的 Routing Service 實作來重新運用。

作為領域模式的額外考量#

當我們在領域層使用技術設計模式時,需要增加一層額外的動機與意義

  • 當 STRATEGY 對應到實際的商業策略或政策時,該模式就不僅是有用的實作技巧,更具備了領域表達力
  • GoF 指出的設計考量依然適用,例如:
    • 客戶端必須了解不同的 STRATEGIES(這也是建模關注點)
    • STRATEGIES 可能增加應用程式中的物件數量——可透過將 STRATEGIES 實作為**無狀態物件(stateless objects)**讓 context 共享來降低開銷
  • Design Patterns 書中對實作方式的廣泛討論在此同樣適用,因為我們仍然在使用 STRATEGY;我們的動機部分不同,會影響某些選擇,但設計模式中積累的經驗仍可為我們所用

Composite#

GoF 定義#

Compose objects into tree structures to represent part-whole hierarchies. COMPOSITE lets clients treat individual objects and compositions of objects uniformly. [Gamma et al. 1995]

領域模型中的動機#

在建模複雜領域時,我們經常遇到一個重要物件由部分組成,而這些部分本身又由部分組成,有時甚至嵌套到任意深度。在某些領域中,每個層級在概念上是不同的;但在其他情況下,部分與整體是相同種類的東西,只是更小

當巢狀容器的關聯性未反映在模型中時,會產生以下問題:

  • 共同行為必須在層級結構的每一層重複實作
  • 巢狀結構是僵化的(例如容器通常無法包含同級的其他容器,層級數量固定)
  • 客戶端必須透過不同的介面來處理不同層級,即使它們在概念上沒有差異
  • 遞迴遍歷層級結構以產生聚合資訊變得非常複雜

何時適用#

在領域中應用任何設計模式時,首要關注的是該模式的概念是否真正契合領域概念。遞迴地遍歷某些關聯物件可能很方便,但是否存在真正的整體-部分層級結構(whole-part hierarchy)?是否找到了一個抽象,使所有部分真正屬於同一概念類型

核心做法#

Define an abstract type that encompasses all members of the COMPOSITE. Methods that return information are implemented on containers to return aggregated information about their contents. “Leaf” nodes implement those methods based on their own values. Clients deal with the abstract type and have no need to distinguish leaves from containers.

  • 定義一個涵蓋 COMPOSITE 所有成員的抽象類型
  • 容器上的方法回傳其內容的聚合資訊
  • 葉節點基於自身的值實作那些方法
  • 客戶端只與抽象類型互動,無需區分葉節點與容器

COMPOSITE 在每個結構層級提供相同的行為,對大小不同的部分都能提出有意義的問題,並透明地反映其組成。這種**嚴格的對稱性(rigorous symmetry)**是該模式力量的關鍵。

範例:Shipment Routes Made of Routes#

一條完整的貨運路線非常複雜:容器必須先以卡車運送到鐵路轉運站,再以鐵路運送到港口,然後以船隻運送到另一個港口,可能轉運到其他船隻,最後在另一端以陸路運送。

Figure 12.3: A schematic of a route made up of legs

開發團隊建立了一個物件模型來表達這些任意長度的 Leg 串聯成 Route 的結構。

Figure 12.4: A class diagram of a Route made up of Legs

使用此模型,開發者能夠根據訂艙請求建立 Route 物件,並將 Legs 處理成逐步處理貨物的操作計畫。然後他們發現了一些事情。

開發者 vs. 領域專家的認知差距#

開發者一直認為 route 是一串無差別的 legs

Figure 12.5: The developers' conception of a route

但領域專家認為 route 是五個邏輯段落(logical segments)的序列

Figure 12.6: The business experts' conception of a route

進一步了解後發現:

  • 這些子路線(subroutes)可能在不同時間由不同人規劃,因此必須被視為獨立的
  • Door legs 與其他 legs 有顯著差異——涉及當地雇用的卡車甚至客戶自行運輸,相較於精密排程的鐵路和船運

反映所有這些區別的物件模型開始變得複雜:

Figure 12.7: The elaborated class diagram of Route

結構上這個模型還不算太糟,但處理操作計畫的一致性喪失了

  • 程式碼或行為描述變得複雜許多
  • 任何對 route 的遍歷都涉及多個不同類型物件的集合

引入 COMPOSITE#

對於某些客戶端來說,將不同層級統一視為由 routes 組成的 routes 會很方便。概念上這是合理的——Route 的每一層都是將容器從一點移動到另一點,一直到個別的 leg 都是如此。

Figure 12.8: A class diagram using COMPOSITE

靜態類別圖雖然不像之前那樣能告訴我們 door legs 和其他 segments 如何組合,但模型不僅僅是靜態類別圖。我們會透過其他圖表和(現在簡單得多的)程式碼來傳達組裝資訊。

Figure 12.9: Instances representing a complete Route

此模型捕捉了所有不同種類 “Route” 之間的深層關聯性。產生操作計畫又重新變得簡單,其他遍歷 route 的操作也是如此。

COMPOSITE 帶來的彈性#

以 route 由其他 routes 組成的方式,可以:

  • 擁有不同細節程度的 route 實作
  • 截斷 route 的尾端並接合新的結尾
  • 實現任意深度的巢狀細節
  • 利用各種可能有用的選項

當然,我們還不需要這些選項。在我們需要 route segments 和獨特的 door legs 之前,沒有 COMPOSITE 也運作得很好。設計模式應只在需要時才應用。

flowchart TD
    P1["開發者觀點\nRoute = Leg 的扁平列表"] --> Problem["衝突:專家需要\n五個邏輯段落"]
    Problem --> Attempt["嘗試:增加 Subroute 層次\n模型變得複雜"]
    Attempt --> Insight["洞察:Route 和 Leg\n共享相同介面"]
    Insight --> Solution["COMPOSITE 模式\nRoute 包含 Route 或 Leg"]
    Solution --> Benefit["統一介面\n任意嵌套、靈活組合"]

為什麼不是 FLYWEIGHT?#

Evans 在第 5 章曾提及 FLYWEIGHT 模式,但在此特別澄清:FLYWEIGHT 是一個與領域模型沒有對應關係的設計模式範例。

  • 當有限的一組 VALUE OBJECTS 被大量使用時(如房屋設計圖中的電源插座),以 FLYWEIGHT 實作是合理的
  • 這是 VALUE OBJECTS 可用的實作選項,但不適用於 ENTITIES
  • 對比 COMPOSITE:概念物件由其他概念物件組成,模式同時適用於模型與實作——這是領域模式的基本特徵

作者不打算編制一份可作為領域模式的設計模式清單。唯一的要求是:模式必須能表達概念性的領域意涵,而非僅僅是技術問題的技術解法。