本章以一個會計系統的案例,展示如何運用 Martin Fowler 在 Analysis Patterns: Reusable Object Models 中提出的模式,來加速 domain model 的深化過程。Analysis Patterns 不是現成的解決方案,而是從實務經驗中提煉出來的模型片段(model fragments),能幫助開發者跳過昂貴的反覆試錯,直接站在前人的肩膀上出發。
Analysis Patterns 的本質#
深層模型(deep models)與靈活設計(supple designs)不會憑空出現,通常需要大量的領域學習、對話與反覆試驗。然而,當有經驗的開發者看到一個熟悉的職責或關係網絡時,他可以回憶過去如何解決類似問題——哪些模型曾被嘗試過、哪些可行、實作中遇到哪些困難又如何解決。這些經驗中的一部分已被文件化並分享出來,讓其他人也能受益。
Martin Fowler 如此定義 Analysis Patterns:
Analysis patterns are groups of concepts that represent a common construction in business modeling. It may be relevant to only one domain or it may span many domains.
與 Part II 的基本建構塊模式和 Chapter 10 的 supple design 原則不同,Analysis Patterns 層級更高、更為專門化,它們使用若干物件來表達某個概念。關鍵特性包括:
- 起點而非終點:提供一個已經具表達力且可實作的模型作為出發點,再從那裡進行 refactoring 和實驗
- 概念性而非技術性:它們不是技術解決方案,而是指引你在特定領域中建構模型的指南
- 包含實作討論:儘管名稱強調「分析」,Fowler 的模式中實際上有大量的實作討論甚至程式碼,因為他理解脫離實務設計的分析有多危險
- 攜帶成熟經驗:最好的 Analysis Patterns 能將其他專案的經驗帶到你的專案中,結合模型洞察與對設計方向、實作後果的深入討論
將模型構想抽離設計脈絡來討論,不僅讓它們更難應用,還有打開 analysis 與 design 之間致命鴻溝的風險——這與 MODEL-DRIVEN DESIGN 的精神背道而馳。
範例:以 Account 模型計算利息#
初始設計#
一個追蹤貸款與其他計息資產的應用程式,負責計算產生的利息和費用,並追蹤借款人的還款。每晚一個 batch 程序會將這些數據傳遞給舊有的會計系統,指定每筆金額應記入哪個 ledger。這個設計能運作,但使用起來笨拙、難以修改,且無法良好地溝通意圖。

Figure 11.1: The initial class diagram
Fowler 的會計模型#
開發者決定閱讀 Analysis Patterns 第六章「Inventory and Accounting」,以下是她找到的最相關部分:
基本會計模型:各種商業應用都會追蹤 Account,Account 持有有價值的東西(通常是金錢)。在許多應用中,光追蹤 Account 的金額是不夠的,必須記錄並控制每一筆變動——這就是最基本的會計模型的動機。

Figure 11.2: A basic accounting model
核心規則:
- 透過插入 Entry 來增加價值
- 透過插入負數的 Entry 來減少價值
- Entry 永不移除,因此完整歷史都被保留
- Balance 是所有 Entry 的綜合效果(可以即時計算或快取,這是由 Account 介面封裝的實作決策)
Transaction 模型:會計的基本原則是守恆——錢不會無中生有,也不會無故消失,只能從一個 Account 移動到另一個。這就是複式簿記(double-entry book-keeping)的概念:每一筆 credit 都有對應的 debit。

Figure 11.3: A transaction model
開發者的第一次嘗試#
讀完這些內容後,Developer 1 有了幾個新想法。她把這章給同事 Developer 2 看(Developer 2 負責利息計算邏輯和 nightly batch 程式)。兩人一起草擬了一個新模型,將讀到的模型元素納入其中。

Figure 11.4: The new model proposal
與領域專家的對話#
兩位開發者拉進 domain expert 來討論新模型。對話中浮現幾個關鍵問題:
- Transaction 的使用不太正確:定義說的是在不同 Account 之間移動金錢,而非在同一 Account 中的兩筆互相平衡的 Entry
- Transaction 的原子性問題:書中強調 Transaction 應一次性建立,但利息還款可能延遲數天
- Accrual 的概念:領域專家提出了一個重要的術語——accrual(應計)意指在費用或收入發生時就記錄,而不管實際的現金流動何時發生。例如每天 accrue 利息,但月底才收到付款
- 不需綁定 accrual 與 payment:專家指出它們是會計系統中獨立的記帳,Account 上的 balance 才是主要關注點
領域專家的一句話「Why do we need to tie together the accrual to the payment?」讓開發者意識到 Transaction 是條死胡同。但 Entry 物件和 Account 概念確實改善了模型的溝通效果。
移除 Transaction,保留 Account#
Developer 1 提出了修改後的設計,將 accruals 與 payment 分離。

Figure 11.5: Original class diagram, accruals separated from payment
這樣做的好處是:移除了 calculator 中為了關聯付款所需的所有複雜邏輯,並引入了 accruals 這個更能揭示意圖的術語。
但領域專家想要看到 Account 物件——能在一個地方看到 accruals、payments 和 balance。於是開發者改用基於 Account 的設計。

Figure 11.6: The account-based diagram, without Transaction
實作中的妥協#
兩位開發者開始基於新模型進行 refactoring。隨著深入程式碼和收緊設計,他們獲得了進一步的洞察:
- Entry 被子類化為 Payment 和 Accrual,因為更仔細的檢視揭示了應用中對它們略有不同的職責,且兩者都是重要的領域概念
- 費用(fee)和利息(interest)之間在概念或行為上沒有區別——它們只是出現在適當的 Account 中
然而,開發者被迫放棄了最後這項抽象。因為資料存在關聯式資料表中,且專案標準要求資料表在不執行程式的情況下也能被解讀。在他們使用的 object-relational mapping 框架下,唯一的做法是建立具體子類(Fee Payments、Interest Payments 等)。

Figure 11.7: The class diagram after the implementation
這個小插曲代表了我們在實務中經常遭遇的現實阻力。我們必須做出經過計算的妥協,然後繼續前進,不讓它偏離我們的 MODEL-DRIVEN DESIGN。
新設計的優勢在於:最複雜的功能都在 SIDE-EFFECT-FREE FUNCTIONS 中,剩餘的 command 程式碼簡潔(因為它呼叫各種 FUNCTIONS),並以 ASSERTIONS 來描述特徵。
範例:Nightly Batch 的洞察#
發現 batch 中隱含的領域邏輯#
新的 Account-based 模型穩定運行幾週後,更清晰的設計反而讓其他問題更加顯眼。負責調整 nightly batch 的 Developer 2 開始看到 batch 的行為與 Analysis Patterns 中某些概念之間的關聯。
有時候程式中有些部分,我們甚至不懷疑它們有可能從 domain model 中受益。它們起初可能非常簡單,然後機械式地演化。它們看起來像複雜的 application code,而非 domain logic。Analysis Patterns 在揭示這些盲點方面特別有用。
Posting Rules 模式#
會計系統經常提供同一基本財務資訊的多個視角。一個帳戶追蹤收入,另一個帳戶追蹤該收入的估計稅額。如果系統需要自動更新估計稅額帳戶,這兩個帳戶的實作就會變得高度糾纏。即使在較簡單的系統中,這種 cross-posting 也很棘手。馴服這種依賴糾葛的第一步,是引入一個新物件來讓規則變得明確。

Figure 11.8: The class diagram of the basic posting rule
Posting Rule 的運作方式:
- 被其「input」Account 中的新 Entry 觸發
- 根據自身的計算 Method 衍生出一個新 Entry
- 將新 Entry 插入其「output」Account
例如:薪資 Account 中的一筆 Entry 可能觸發一個 Posting Rule,計算 30% 的估計所得稅,並將其插入預扣稅款 Account。
三種觸發模式#
Posting Rule 建立了 Account 之間的概念性依賴,但最棘手的部分在於更新的時機與控制。Fowler 討論了三種選項:
| 觸發模式 | 說明 |
|---|---|
| Eager Firing | 最直觀但通常最不實用。每當 Entry 被插入 Account 時,立即觸發 Posting Rules 並立即完成所有更新 |
| Account-based Firing | 允許延遲處理。在某個時間點,向 Account 發送訊息,觸發它的 Posting Rules 處理自上次觸發以來插入的所有 Entry |
| Posting-Rule-based Firing | 由外部代理發起,告知 Posting Rule 執行。Posting Rule 負責查找自上次觸發以來其 input Account 中的所有 Entry |
雖然一個系統中可以混合使用不同的觸發模式,但每一組特定的規則都需要有一個明確定義的發起點和識別 input Account Entry 的責任。這三種觸發模式加入 UBIQUITOUS LANGUAGE,對於模式成功的重要性,不亞於模型物件定義本身——它消除歧義,並將決策直接引導到一組明確定義的選擇上。
開發者的討論與設計演進#
Developer 2 長期以來想為 batch 做 model-driven design,將 domain layer 分離出來,讓 script 本身成為 domain 之上的簡單層。但他一直無法想出 domain model 該長什麼樣——似乎只是一些程序,不太適合用物件表達。讀了 Posting Rules 的章節後,他有了新想法。

Figure 11.9: A shot at using Posting Rules in the batch
對話中的關鍵決策:
- Posting Service 是一個 FACADE,暴露會計應用的 API 並以 SERVICE 的形式呈現,同時提供了一個 INTENTION-REVEALING INTERFACE 用於向 legacy 系統記帳
- 選擇 Posting-Rule-based Firing:Eager Firing 對 Accruals 可行(因為 batch 告訴 Asset 插入它們),但對白天輸入的 Payments 不行。而且把計算方法耦合到 batch 在概念上也不正確
- 簡化 Method:在此案例中,每筆 posting 的金額都是全額,不需要像稅率計算那樣的 Method。Posting Rule 本身負責知道正確的 ledger name,這對應到原始模式中的 Method 概念
- Ledger 選擇邏輯:選擇正確 ledger name 的邏輯越來越複雜,已經是收入類型(fee 或 interest)與 asset class(業務對每個 Asset 套用的分類)的組合。新模型正好能處理這個問題
最終設計#
經過幾天的討論,兩位開發者建構出一個模型並重構了程式碼:batch 只需遍歷 Assets,對每一個發送幾個一目了然的訊息,然後提交資料庫交易。複雜度被轉移到了 domain layer,一個物件模型讓它既更明確又更抽象。

Figure 11.10: The class diagram with Posting Rules

Figure 11.11: Sequence diagram showing rule firing
開發者相當程度地偏離了 Analysis Patterns 中模型的細節,但他們感覺保留了概念的本質。他們對於讓 Asset 參與 Posting Rule 的選擇有些不安——之所以這樣做,是因為 Asset 知道每個 Account 的性質(fee 或 interest),而且是 script 的自然存取點。若將 rule 物件直接與 Account 關聯,每次實例化物件時(每次 batch 執行時)都需要與 Asset 物件協作。因此他們讓 Asset 透過 SINGLETON 存取查找兩個相關的規則,並傳遞適當的 Account——這是一個務實的決策,讓程式碼更為直接。
他們都覺得在概念上,只將 Posting Rules 與 Accounts 關聯會更好,同時讓 Asset 專注於產生 Accruals 的工作。他們希望後續的 refactoring 和更深入的洞察能帶他們回到這一點,找到一種在不失去程式碼明確性的前提下實現這種乾淨分離的方法。
Analysis Patterns 是可汲取的知識#
當你有幸找到一個 Analysis Pattern 時,它幾乎不會是你特定需求的現成答案。然而,它能提供:
- 調查中的有價值線索
- 乾淨抽象的詞彙
- 關於實作後果的指引,幫你避免未來的痛苦
這些全都注入知識消化(knowledge crunching)和 refactoring toward deeper insight 的循環中,刺激開發。結果往往類似 Analysis Pattern 中記載的形式但適應了具體情境,有時甚至與 Analysis Pattern 本身沒有明顯的關聯,卻是受到其中洞察的啟發。
使用 Analysis Patterns 術語的注意事項#
當你使用知名 Analysis Pattern 的術語時,務必保持它所指定的基本概念完整,無論表面形式如何變化。原因有二:
- 該模式可能蘊含著能幫你避免問題的理解
- 更重要的是,當 UBIQUITOUS LANGUAGE 包含被廣泛理解或至少有良好解釋的術語時,它會得到增強
如果你的模型定義隨著模型的自然演進而改變,也要不怕麻煩地一併更改名稱。
Analysis Patterns 與程式碼重用的區別#
Analysis Patterns 的知識重用與透過 framework 或 component 重用程式碼完全不同:
- 模型(甚至是通用化的 framework)是一個完整的工作整體
- Analysis Pattern 是一套模型片段工具箱(a kit of model fragments)
Analysis Patterns 聚焦於最關鍵和最困難的決策,闡明替代方案與選擇,並預見如果必須自己去發現會代價高昂的下游後果。