前面我們已經簡單介紹了領域建模的好處,那麼下一步我們如何進行領域建模呢?
一、初步建模好的模型應該是建立在對業務深入理解的基礎上。就我自己的經驗而言,建模是一個不斷迭代的過程,一開始可以簡單點來。
首先抓住一些核心概念,這些業務知識和核心概念可以透過和業務專家溝通,也可以透過頭腦風暴的形式從User Story或者Event Storming去扣。
然後假設一些業務場景走查一下,再寫一些虛擬碼驗證一下run一下,看看順不順,如果很順滑,說明沒毛病,否則就要看看是不是需要調整一下模型,隨著專案的進行和對業務理解的不斷深入,這種迭代將持續進行。
舉個例子,比如讓你設計一箇中介系統,一個典型的User Story可能是“小明去找工作,中介說你留個電話,有工作機會我會通知你”,這裡面的關鍵名詞很可能就是我們需要的領域物件:
- 小明是求職者。
- 電話是求職者的屬性。
- 中介包含了中介公司,中介員工兩個關鍵物件。
- 工作機會肯定也是關鍵領域物件;
- 通知這個動詞暗示我們這裡用觀察者模式會比較合適。
然後再梳理一下領域物件之間的關係,一個求職者可以應聘多個工作機會,一個工作機會也可以被多個求職者應聘,M2M的關係,中介公司可以包含多個員工,O2M的關係。對於這樣簡單的場景,這個建模就差不多了。
當然我們的業務場景往往比這個要複雜,而且不是所有的名詞都是領域物件也可能是屬性,也不是所有的動詞都是方法也可能是領域物件,再者,看的見實體好找,看不見的、隱藏的,需要深入理解業務,需要“無中生有”才能得到的抽象就沒那麼容易發現了,所以要具體問題具體對待,這個進化的過程需要我們有很好的業務理解力,抽象能力以及建模的經驗(知道為什麼公司的job model裡那麼強調技術人員的業務理解力和抽象能力了吧。
比如通常情況下,價格和庫存只是訂單和商品的一個屬性,但是在阿里系電商業務場景下,價格計算和庫存扣減的複雜程度可以讓你懷疑人生,因此作為電商中臺,把價格和庫存單獨當成一個域(Domain)去對待是很必要的。
當然這個只是最初級的模型,接下來我會透過DDD中的一些核心概念的介紹,讓大家更清楚的瞭解建模的過程。
二、領域事件(Domain Event)An event is something that has happened in the past. A domain event is, logically, something that happened in a particular domain, and something you want other parts of the same domain (in-process) or domain in aonther bounded context to be aware of and potentially react to.
Domain Event是由一個特定領域觸因為一個使用者Command觸發的發生在過去的行為產生的事件,而這個事件是系統中其它部分感興趣的。
為什麼Domain Event如此重要? 因為在現在的分散式環境下,沒有一個業務系統是割裂的,而Messaging絕對是系統之間耦合度最低,最健壯,最容易擴充套件的一種通訊機制。因此理論上它是分散式系統的必選項。
但是目前大部分系統的Event都設計的很隨性,沒有統一的指導和規範,導致Event濫用和無用的情況時有發生,而Domain Event給我們一個很好的方向,指引我們該如何設計我們系統的Event。
(1)Event命名
Your Domain Event type names should be a statement of a past occurrence, that is, a verb in the past tense.
因為表示的是過去事件,所以推薦命名為Domain Name + 動詞的過去式 + Event。這樣比較可以確切的表達業務語義。
下面是幾個舉例:
1. CustomerCreatedEvent,表示客戶建立後發出的領域事件。
2. OpportunityTransferedEvent,表示機會轉移後發出的領域事件。
3. LeadsCreatedEvent,表示線索建立後發出的領域事件。
(2)Event內容
Event的內容有兩種形式:
Enrichment:也就是在Event的payload中儘量多多放data,這樣consumer就可以自治(Autonomy)的處理訊息了。
Query-Back:這種是在Event中透過回撥拿到更多的data,這種形式會加重系統的負載,performance也會差一些。
所以如果要在Enrichment和Query-Back之間做選擇的話,首先推薦使用Enrichment。
(3)Event Sourcing
Event Sourcing是在Domain Event上面的一個擴充套件,是一個可選項。也就是要有一個Event Store儲存所有的Events,其實如果你是用MetaQ作為Event機制的話,這些Events都是儲存在MetaQ當中的,只是MetaQ並沒有提供很好的Event查詢和回溯,所以如果決定使用Event Sourcing的話,最好還是自己單獨建立一個Event Store。
使用Event Sourcing主要有以下好處,如果用不到的話,完全可以不用,但是Domain Event還是強烈建議要使用。
Event Sourcing儲存了所有發生在Core Domain上面的事件。
基於這些事件,我們可以做系統回放,系統Debug,以及做使用者行為的分析(類似於打點)。
(4)Event Storming
事件風暴是《DDD Distilled》書中提出的一個業務分析的方法論,其主要作用是從Domian事件出發,來分析使用者Command,來找到Ubiquitous Languange,來抽象Domain Entity以及Bounded Context。
可以和User Story的方法論結合起來使用,其最大的優點是,這種分析方式即使是non-tech的人,比如產品,業務專家等也能聽得懂,也能參與進來。相比較一上來就使用UML畫領域模型圖而言。
三、聚合根(Aggreagte)聚合根(Aggregate Root)是DDD中的一個概念,是一種更大範圍的封裝,把一組有相同生命週期、在業務上不可分隔的實體和值物件放在一起考慮,只有根實體可以對外暴露引用,也是一種內聚性的表現。
確定聚合邊界要滿足固定規則(Invariant),是指在資料變化時必須保持的一致性規則,具體規則如下:
根實體具有全域性標識,最終負責檢查規定規則
聚合內的實體具有本地標識,這些標識在Aggregate內部才是唯一的
外部物件不能引用除根Entity之外的任何內部物件
只有Aggregate的根Entity才能直接透過資料庫查詢獲取,其他物件必須透過遍歷關聯來發現
Aggegate內部的物件可以保持對其他Aggregate根的引用
Aggregate邊界內的任何物件修改時,整個Aggregate的所有固定規則都必須滿足
還是看銀行的例子,Account(賬號)是CustomerInfo(客戶資訊)Entity和Address(值物件)的聚合根,Tansaction(交易)是流水(Journal)的聚合根,因為流水是因為交易才產生的,具有相同的生命週期。
最後提醒一下,聚合根是一個邏輯概念,主觀性很強,所以在建模過程中很容易產生分歧,因此在日常工作中千萬不要教條,把握住一條主要原則,我們的最終目的是為了業務語義顯現化,如果因為聚合根把模型弄的晦澀難懂那就得不償失了。
四、領域服務(Domain Service)(1)什麼是領域服務
有些領域中的動作,它們是一些動詞,看上去卻不屬於任何物件。它們代表了領域中的一個重要的行為,所以不能忽略它們或者簡單地把它們合併到某個實體或者值物件中。當這樣的行為從領域中被識別出來時,最佳實踐是將它宣告成一個服務。這樣的物件不再擁有內建的狀態。它的作用僅僅是為領域提供相應的功能。Service往往是以一個活動來命名,而不是Entity來命名。例如開篇轉賬的例子,轉賬(transfer)這個行為是一個非常重要的領域概念,但是它是發生在兩個賬號之間的,歸屬於賬號Entity並不合適,因為一個賬號Entity沒有必要去關聯他需要轉賬的賬號Entity,這種情況下,使用MoneyTransferDomainService就比較合適了。
識別領域服務,主要看它是否滿足以下三個特徵:
1. 服務執行的操作代表了一個領域概念,這個領域概念無法自然地隸屬於一個實體或者值物件。
2. 被執行的操作涉及到領域中的其他的物件。
3. 操作是無狀態的。
(2)領域服務陷阱
在使用領域服務時要特別當心,一個比較常見的錯誤是沒有努力為行為找到一個適當的物件,就直接抽象成領域服務,這會使我們的程式碼逐漸轉化為過程式的程式設計,一個極端的例子是把所有的行為都放到領域服務中,而領域模型退化成只有屬性的貧血DO,那DDD就沒有任何意義了。所以一定要深入思考,既不能勉強將行為放到不符合物件定義的物件中,破壞物件的內聚性,使其語義變得模糊。也不能不加思考的都放到領域服務中,從而退化成面向過程的程式設計。
(3)應用服務和領域服務如何劃分
在領域建模中,我們一般將系統劃分三個大的層次,即應用層(Application Layer),領域層(Domain Layer)和基礎設施層(Infrastructure Layer)。可以看到在Application層和Domain層都有服務(Service),這兩個Service如何劃分呢,什麼樣的功能應該放在應用層,什麼樣的功能應該放在領域層呢?
決定一個服務(Service)應該歸屬於哪一層是很困難的。如果所執行的操作概念上屬於應用層,那麼服務就應該放到這個層。如果操作是關於領域物件的,而且確實是與領域有關的、為領域的需要服務,那麼它就應該屬於領域層。總的來說,涉及到重要領域概念的行為應該放在Domain層,而其它非領域邏輯的技術程式碼放在Application層,例如引數的解析,上下文的組裝,呼叫領域服務,訊息傳送等。
還是銀行轉賬的case為例,下圖給出了劃分的建議:
五、限界上下文(Bounded Context)領域實體是有限界上下文的,比如Apple這個實體不同的上下文,表達的含義就完全不一樣,在水果店它就是水果,在蘋果專賣店它就是手機。
所以限界上下文(Bounded Context)在DDD裡面是一個非常重要的概念,Bounded Context明確地限定了模型的應用範圍,在Context中,要保證模型在邏輯上統一,而不用考慮它是不是適用於邊界之外的情況。在其他Context中,會使用其他模型,這些模型具有不同的術語、概念、規則和Ubiquitous Language的行話。
Mapping的方式有很多種,有Shared Kernal(共享核心),Conformist(追隨者),以及Anti-Corruption(防腐層)等等。
我個人比較推崇Domain Event + AC,這樣可以將系統之間的耦合降到最低。
以我們真實的業務場景舉個例子,比如會員這個概念在ICBU網站是指網站上的Buyer,但是在CRM領域是指Customer,雖然很多的屬性都是一樣的,但是二者在不同的Context下其語義和概念是有差別的,我們需要用AC做一下轉換:
七、限界上下文和微服務先來說一下微服務,拋開以Docker為代表的底層容器化技術不看,微服務和我們之前的SOA沒有本質區別。
那麼如何劃分系統,才能得到一個比較合適的粒度,不會太粗,也不會太細呢。此時我們可以考慮DDD的戰略設計,即從戰略角度整體描述業務領域全貌,然後透過限界上下文將不同的實體歸類到相對應的域裡面。
比如在CRM領域,我們按照下面的戰略設計圖,我會自然的把CRM系統劃分成銷售服務,組織許可權服務,營銷服務,售賣服務。
八、領域模型模型重構
最後我想強調的是,建模不是一次性的工作,也不可能是一次性的工作,業務在演化,隨之而來的模型也需要演化和重構,當模型和業務不匹配的時候,你還是要霸王硬上弓的往裡面塞,其結果可想而知。
模型統一
建模的過程很像盲人摸象,不同背景人用不同的視角看同一個東西,其理解也是不一樣的。比如兩個盲人都摸到大象鼻子,一個人認為是像蛇(活的能動),而另一個人認為像消防水管(可以噴水),那麼他們將很難整合。雙方都無法接受對方的模型,因為那不符合自己的體驗。事實上,他們需要一個新的抽象,這個抽象需要把蛇的“活著的特性”與消防水管的“噴水功能”合併到一起,而這個抽象還應該排除先前兩個模型中一些不確切的含義和屬性,比如毒牙,或者捲起來放到消防車上去的行為,這就是模型的統一。統一完的模型也許還不叫大象鼻子,但是已經很接近大象鼻子的屬性和功能了,隨著我們對模型物件、對業務理解的越來越深入、越來越透徹,我們會不斷的調整演化我們的模型,所以建模不是一個one-time off的工作,而是一個持續不斷演化重構的過程。
模型演化
世界上唯一不變的就是變化,模型和程式碼一樣也需要不斷的重構和精化,每一次的精化之後,開發人員應該對領域知識有了更加清晰的認識。這使得理解上的突破成為可能,之後,一系列快速的改變得到了更符合使用者需要並更加切合實際的模型。其功能性及說明性急速增強,而複雜性卻隨之消失。這種突破需要我們對業務有更加深刻的領悟和思考,然後再加上重構的勇氣和能力,勇氣是專案工期很緊你敢不敢重構,能力是你有沒有完備的CI保證你的重構不破壞現有的業務邏輯。
原文連結:
複雜性應對之道 - 領域建模
http://www.uml.org.cn/sjms/201812133.asp