
阿里妹導讀
本文作者結合在團隊的實踐過程,分享了自己對領域驅動設計的一些思考。
瞭解過領域驅動設計的同學都知道,人們常常把領域驅動設計分為兩部分:戰術設計和戰略設計。這兩個概念本身都是抽象的,有人把戰術設計看作是領域內的設計過程,而戰略設計看作是領域間關係的設計過程。也有一種認知是把戰術設計看作是編碼的設計,把戰略設計看作是架構的設計。實際上領域驅動設計的作者Eric Evans本無意將這兩者進行割裂,相反兩者之間相輔相成,缺一不可。我將在本文中結合團隊的實踐過程,分享我對領域驅動設計的一些思考。
轉變思維
被忽視的面向物件
我們在剛開始學習面向物件的時候,知道面向物件的三個特性:繼承、封裝、多肽,也知道面向物件的SOLID原則,但很不幸的是,當我們在實際工作以後,這些特性和原則好像並無用武之地。目前我在公司看到過的大部分程式碼中的物件只有兩種型別:服務類(Service Object)和資料類(Data Object),所有的資料物件,被無腦的開放了所有的Getter和Setter方法,加之lombok等語法糖的推波助瀾,物件的封裝變得更加困難了。而所有的業務邏輯都被堆在了各種Service中,當然好的團隊依然會對這些Service類做很好的分層設計,使程式碼的演進還能夠正常的進行。
實際上我並不是要說這種開發方式不好,相反它能夠在程式設計師中被廣泛認可,其優勢不言而喻,它能夠讓一個只要掌握程式語言的新手,快速的承接需求並交付,無需在程式碼設計和怎麼寫的問題上花費更多的精力和學習成本。
大部分情況下,團隊內的架構師只需要做好介面設計和資料庫的設計,這個需求就可以完全交給一個新人去實現了。
我把這種方式看作是一種透過確定【輸入】和【輸出】來控制軟體開發確定性的方式,
-
輸入即程式對外提供的可以執行程式的入口,我們常見的像RPC介面、HTTP介面、訊息監聽、定時任務等。
-
輸出是程式對外部環境或者狀態的影響,可以是資料庫的寫入、訊息的廣播推送、外部系統的呼叫等等。
在一個系統剛開始的階段,這種方式能夠以非常高的效率完成交付,這個階段業務的本質複雜性低,技術的複雜性也低,程式的輸入和輸出鏈路比較單一。更重要的是在人的方面,每個人都能夠很好的理解這種開發方式,只要從輸入到輸出的轉換沒有問題,程式設計師們不會去關注其中潛在的設計問題,無論是新人還是老手,開發這樣的軟體都能得心應手。相比於使用領域驅動設計的思維進行開發,面向過程的這種開發方式更簡單直接,對人和團隊的要求更低,在人員變動頻繁的現狀中,它能帶來更快速的交付。
複雜度的膨脹
然而隨著系統逐漸的演進,業務的核心複雜性變高,系統之間的聯絡逐漸變多,面向過程的這種開發方式就顯得捉襟見肘了。不知道大家能否在自己團隊中找到這樣的程式碼:
-
上千行的方法 -
上百個屬性的類 -
迴圈依賴的Service -
無法控制的資料一致性 -
越來越多的分支邏輯 -
…
這些問題本質上並不是我們採用哪種開發方式就能解決的,但它們一定能說明我們當前的程式碼設計是存在問題的,這就像埋在我們系統中的一個個定時炸彈,如果你足夠小心,團隊的質量保障足夠充足,這顆炸彈在你工作的期間可能並不會引爆,但根據墨菲定律,它們早晚是會引爆的。潛在的風險是一方面,另一方面是我們的交付速度,理解成本,溝通成本,知識的傳遞,都會因為這些混亂的程式碼而變得緩慢和困難。
但是程式設計師們總是會有辦法的,用戰術上的勤奮來彌補戰略上的懶惰,花更多的時間去討論,去梳理,寫更多的文件,做更多的測試,掉更多的頭髮。當系統最終無法應對業務的變化時,要麼一走了之,要麼從頭再來搞個2.0。
應對軟體複雜度的方法有很多,即使是使用面向過程的開發方式,也有很多設計模式和方法論能夠去解決這些問題。如果你還沒有找到一個特別好的方式,不妨嘗試一下領域驅動設計。
基於面向物件
在進行領域驅動設計落地的過程中,我感覺到最大的一個困難點是面向物件思維的轉變,領域驅動設計實際上是基於面向物件設計的一種更高維度的設計模式,但我們之中大部分的開發者,已經習慣於按照面向過程的方式來進行開發,即使我們在很多場合都在強調我們在使用面向物件,但實際上卻事與願違。
經驗越豐富,越資深的工程師,越無法跳出之前長期積累的認知,這種先入為主的思維定式改變起來尤為困難。
還有源源不斷的新人逐漸開始進入這個行業,成為一個軟體工程師,他們被要求能夠儘快的開始交付和產出,他們也只能去模仿團隊現在的程式碼,逐漸熟練以後,也只會把這種開發方式奉為聖經,再次傳承下去。
隨著實踐領域驅動設計逐漸進入到深海區,我越來越感受到,面向物件至關重要,長期面向介面程式設計、面向資料庫程式設計、面向中介軟體程式設計,已經讓大家的思維很難去轉變。即使我們有再好的領域設計,邊界劃分,如果無法將其在程式碼中表現出來,那也只會是空中樓閣,無法發揮領域驅動設計的真正作用。
領域模型
之前提到,我們現在的開發現狀是透過【輸入】和【輸出】來進行設計,而領域驅動設計則是在其基礎上增加了一層:【領域模型】。即所有的輸入都要轉換為領域模型,所有的輸出也都要透過領域模型去完成。領域驅動設計的所有模組、模式、方法都是基於領域物件,為領域物件服務的。
領域模型本身作為對現實世界中我們所解決問題空間的抽象,它的演進與問題空間的演進原則上是一致的,之所以使用面向物件來作為領域模型的承載,主要原因還是面向物件更加符合當下人們對現實世界的認知,理解和使用都更加簡單。現實世界中大部分的“系統”,都是可以用物件,以及物件之間的關係來描述,認識、理解、描述現實世界中的客觀事物是人類哲學最早開始思考的問題,先秦時期的名家,古希臘的形而上學,都是基於此目的建立的。今天我們的工作又何嘗不是在混亂複雜的世界中,尋找規律,將其透過有限的模型表達出來,再轉換為機器可以理解的語言,形成軟體或者系統,簡化人與人,人與物,物與物之間的互動過程。
每次想到這些我就會熱血沸騰,雖然生活限制了你人身的自由,但並沒有限制你思維的自由,去認識世界、抽象現實,軟體工程不光只有埋頭敲程式碼的嗎嘍,也可以有像蘇格拉底一樣探索世界本質的思考者。
當然如何建模以我現在掌握的技巧和經驗,實在有點拿不出手,還需再沉澱一下,本文還是主要關注於如何把領域模型在程式碼中進行落地。
領域物件
1. 實體(Entities)
Many objects are not fundamentally defined by their attributes, but rather by a thread of continuity and identity.
領域模型中最核心的是領域物件,而領域物件中最核心的是實體,《領域驅動設計》裡對實體的定義如上,意思是,實體從根本上不由其屬性來定義,而是由連續性和唯一性來定義。
類似於“白馬非馬”的哲學問題,“白馬”是名(Defination),“馬”也是名,只有你看得見摸得著實際存在的那匹白馬才是實(Instance),假設這個世界是一個巨大的Java虛擬機器,唯一能代表那匹白馬的,只有它在記憶體裡的地址。即使這匹馬之後染了黃毛,起了個名字叫“小黑”,它還是它,不因其屬性或者特徵的變化而成為另外一匹馬。直到這匹馬死去,屍骨化為養分,它的存在不再有任何意義,系統收回它所佔用的記憶體,這個實也就徹底不存在了。在領域模型中,需要透過一個唯一標識而不是其屬性來區分,且在其生命週期中具有連續性的物件,我們將它定義為一個實體。概念的解釋過於抽象,我們來透過我們最熟悉的訂單Order為例:
classOrder{
private String id;
private Date createTime;
private Status status;
voidcomplete(){
this.status = Status.COMPLETED;
}
}
// 實體的一生
void lifeOfOrder{
// 建立:物件的首次建立,需要透過一個符號來唯一標識它
Order order = new Order("ID", new Date(), Status.INIT);
// 儲存:儲存到資料庫或者檔案中
new OrderRepository().save(order);
// 重建:衝資料庫或檔案中讀取
Order orderRef = new OrderRepository().get("ID");
// 修改:物件在修改其屬性並重新持久化
orderRef.complete();
new OrderRepository().save(orderRef);
// 刪除:從資料庫或檔案系統中存檔或永久刪除,系統將無法再次重建該物件
new OrderRepository().delete(orderRef);
}
我們在實際應用的過程中,實體往往是需要持久化到資料庫的,因此大部分情況下,我們都以資料庫中的主鍵作為實體的唯一標識,雖然這種方式並不完全符合領域物件應獨立於資料庫存在,但在實際使用的過程中,並不會產生太大的影響。以一個訂單實體Order的建立為例,我們來看幾種不同的唯一標識落地策略:
-
使用資料庫主鍵作為實體唯一標識
注:使用這種策略,實體只有經過持久化以後,才能產生唯一標識,實際使用的過程中很容易出錯,不建議使用。
//領域物件
classOrder{
private Long id;
}
//ORM框架資料庫表物件
classOrderDO{
//資料庫主鍵
private Long id;
}
classOrderFactory{
public Order buildOrder(){
returnnew Order();
}
}
classOrderRepository{
private OrderDao orderDao;
publicvoidinsert(Order order){
OrderDO orderDO = new OrderDO()
orderDao.insert(orderDO);
//從ORM物件中獲取表自增ID回填領域物件
order.setId(orderDO.getId());
}
}
-
使用隨機UUID作為實體唯一標識
//領域物件
classOrder{
private String id;
publicOrder(String id){
this.id = id;
}
}
//ORM框架資料庫表物件
classOrderDO{
//資料庫主鍵
private Long id;
//Order唯一標識
private String orderId;
}
classOrderFactory{
public Order buildOrder(){
returnnew Order(UUID.randomUUID().toString());
}
}
classOrderRepository{
private OrderDao orderDao;
publicvoidinsert(Order order){
OrderDO orderDO = new OrderDO()
orderDO.setOrderId(order.getId());
orderDao.insert(orderDO);
}
}
-
使用Sequence生成實體唯一標識
//領域物件
classOrder{
private String id;
publicOrder(String id){
this.id = id;
}
}
//ORM框架資料庫表物件
classOrderDO{
//資料庫主鍵
private Long id;
//Order唯一標識
private String orderId;
}
classOrderFactory{
//序列生成器,可以參考TDDL Seq:https://mw.alibaba-inc.com/tddl/DeveloperReference/sequence
private SeqGenerator seqGenerator;
public Order buildOrder(){
returnnew Order("PREFIX_" + seqGenerator.nextInt());
}
}
classOrderRepository{
private OrderDao orderDao;
publicvoidinsert(Order order){
OrderDO orderDO = new OrderDO()
orderDO.setOrderId(order.getId());
orderDao.insert(orderDO);
}
}
2. 關聯(Association)
一個實體往往會關聯另外一個實體,這種關聯關係主要包含一對一、一對多、多對多這三種類型,這個相信大家在資料庫設計的過程中已經很熟悉了。在領域模型裡,一對多,多對多的關聯,往往會讓程式碼複雜度急劇上升。
以訂單為例,一個訂單(Order)可以包含多個產品(Product),一個產品也可以屬於多個訂單。
classOrder{
private String id;
privateList<Product> products;
}
classProduct{
private String id;
privateList<Order> orders;
}
表面看起來領域物件如此設計沒有任何問題,符合現實中兩者之間的關係,但是在物件使用的過程中卻很麻煩,尤其是這種物件互相引用的場景。解決這個問題有幾種思路:
-
規定一個遍歷的方向:僅允許透過訂單遍歷該訂單下所有的產品,這樣訂單和貨品之間多對多的關係就簡化為一對多。 -
新增限定:限定訂單隻允許包含一個產品,這種限定可能作用於某種特殊型別的訂單,這樣訂單和產品的關係就會簡化為一對一。 -
消除不必要的關聯:產品對訂單的引用,往往並沒有實際作用的場景,這種情況我們可以消除產品對訂單的關聯關係。
簡化後的領域物件:
classSingleProductOrder{
private String id;
private Product product;
}
classProduct{
private String id;
}
3. 領域物件的持久化(Persistence)
這個章節本來想放到最後去說,但是想想又不得不把這部分提到前面來講,因為這部分可能是我們在設計領域模型過程中最容易出現問題的。我們大部分應用使用的ORM框架,基本上都是用Mybatis,因此我們往往都需要有一個物件來對映資料庫表結構,這裡我將它命名為資料庫物件,我們在程式碼中一般會透過DO、BO等字尾來進行區分。也正因為這個原因,我們很多時候都會直接將資料庫模型作為程式碼設計的目標,程式碼邏輯也是為了操作資料庫物件來寫,導致程式碼中缺失真實業務場景的還原。
所以首先要強調的是一定要將領域模型和資料庫模型分離開,這樣我們的業務程式碼僅需要關注領域模型,到需要持久化的時候再去關心如何將領域模型轉換為資料庫模型。如此,即使之後資料庫的選型發生變化,對程式碼的改動也僅限於物件轉換的那部分邏輯;領域模型的迭代演進也可以更加自由,不受資料庫設計的約束。
領域模型到資料庫模型轉換的過程中需要注意幾個細節:
-
不要將資料庫關注的屬性,無腦新增到領域物件中去,比如id、gmt_created、gmt_modified等。
-
實體間的關聯,在資料庫中經常會透過關係表來表達,但在領域物件中,完全可以透過類的引用關係來表示,不需要將關係抽象為實體(除非這個關係有特殊的業務意義)。
將領域物件轉換為資料庫物件:
class Order{
privateString id;
private List<Product> products;
}
class Product{
privateString id;
}
class OrderDO{
private Long id;
privateString orderId;
privateDate gmtCreated;
privateDate gmtModified;
}
class OrderProductRelationDO{
private Long id;
privateString orderId;
privateString productId;
privateDate gmtCreated;
privateDate gmtModified;
}
class OrderRepository{
void save(Order order){
orderDao.insert(new OrderDO(order.getId()));
order.getProducts().forEach(orderProduct -> {
orderProductRelationDao.insert(new OrderProductRelationDO(order.getId(), orderProduct.getId()));
});
}
}
擴充套件閱讀: 來阿里之前在一個專案中,使用Spring JPA做了領域物件持久化的解決方案,用起來很爽,但是也有很多的問題,這裡不做展開的介紹,僅透過JPA的一些註解來讓大家淺嘗一下,如果感興趣可以自己嘗試一下:Getting Started :: Spring Data JPA@Entity:標識實體類是JPA實體,告訴JPA在程式執行時生成實體類對應表@Table:設定實體類在資料庫所對應的表名@Id:標識類裡所在變數為主鍵@GeneratedValue:設定主鍵生成策略,此方式依賴於具體的資料庫@Column:表示屬性所對應欄位名進行個性化設定@Transient:表示屬性並非資料庫表字段的對映,ORM框架將忽略該屬性@Temporal:當我們使用到java.util包中的時間日期型別,則需要此註釋來說明轉化成java.util包中的型別。@Enumerated:使用此註解對映列舉欄位,以String型別存入資料庫@Embedded、@Embeddable:當一個實體類要在多個不同的實體類中進行使用,而其不需要生成資料庫表@Embeddable:註解在類上,表示此類是可以被其他類巢狀@Embedded:註解在屬性上,表示巢狀被@Embeddable註解的同類型類@ElementCollection:集合對映@CreatedDate、@CreatedBy、@LastModifiedDate、@LastModifiedBy:表示欄位為建立時間欄位(insert自動設定)、建立使用者欄位(insert自動設定)、最後修改時間欄位(update自定設定)、最後修改使用者欄位(update自動設定)
4. 值物件(Value Object)
Many objects have no conceptual identity. These objects describe some characteristic of a thing.
當一個實體內的部分屬性,我們發現它們具有較強的相關性,這些屬性單獨抽象成一個物件可以更好的描述事物,且這個物件並不具備唯一性,我們就將它歸類為值物件,值物件具備以下特徵:
-
不需要唯一標識來代表其唯一性 -
一些有關係的屬性的聚合 -
有自己的特徵 -
對模型有重要的意義 -
是用來描述事物的物件

如圖所示,客戶(Customer)這個物件中,描述客戶地址的三個屬性,可以將其抽象為一個地址物件(Address),在我們實際的程式碼中,這樣做的好處主要包括:
-
關注點分離:透過值物件的提取,可以簡化實體,突出實體核心屬性,開發者只需要把注意力放在實體本身關鍵的屬性上; -
控制複雜度:使實體在持續演進的過程中,不會逐漸膨脹; -
不變性:值物件可以複製,並在物件間傳遞;
class Customer{
privateString id;
private Address address;
public Customer(String id, Address address){
this.id = id;
this.address = address;
}
}
class Order{
privateString id;
private Address customerAddress;
}
class Address {
privateString street;
privateString city;
privateString stateOrProvince;
privateString postalCode;
privateString country;
privateString unitNumber;
privateString latitude;
privateString longitude;
privateString additionalInfo;
}
void buildOrder(Customer customer){
new Order("id", customer.getAddress());
}
5. 聚合(Aggregate)
實體關聯的極簡設計能夠幫助我們描述現實世界事物之間的關係,並且能在一定程度上限制關係的複雜度增長,但隨著業務發展,實體間的關係會越來越複雜,我們依然需要將這種關係表達在模型裡,但是如果還是將這種關聯表達在實體中,實體就會因各種關係帶來的複雜性而膨脹,開發者也無法關注到模型的核心。當多個實體之間在某些場景下需要保持更改的一致性時,除了使用物件關聯外,還可以建立一個物件組,將有著緊密關係的實體和值物件封裝在一起,這個物件組就是領域模型中的聚合。
繼續之前的例子,我們豐富一下訂單模型:客戶購買產品會產生交易訂單,一個交易訂單下會關聯多個訂單項,一個訂單項包含購買的產品及數量,交易訂單完成支付後會建立一個物流單。

classTradeOrder{
private String id;
private Customer customer;
privateList<OrderItem> orderItems;
private LogisticsOrder logisticsOrder;
}
如此我們建立的實體就會變成這樣,從交易單視角來看似乎沒有什麼問題,但這個模型在其他的場景下就會變得臃腫難以使用。假設以下幾種用例(本故事純屬虛構,如有雷同,純屬巧合,實際情況可能更離譜):
-
使用者登出賬號,需要立即終止所有訂單; -
物流單完成簽收後,需要更新交易單狀態; -
某個產品緊急下架,需要刪除所有該產品的訂單項,並更新交易單價格;
為了保證在不同場景下,各個實體間更改的一致性,我們需要將以上的實體按照不同場景做個分組:

如此,幾個實體間複雜的關聯關係被我們以聚合的方式做了分離,聚合擁有兩個重要特徵:
-
邊界:定義聚合內有什麼,與其他聚合區分。
-
聚合根:聚合中的一個特定實體
-
選擇聚合中的一個Entity作為聚合根; -
透過根來控制對邊界內其他物件的訪問; -
只允許外部物件保持對根的訪問; -
對邊界內的其他物件透過根來遍歷關聯來發現;
在實際將聚合在程式碼中落地的過程中,我曾經歷過兩種不同的寫法:
-
一個物件,即是實體,也是聚合,同時是該聚合中的聚合根。
class TradeOrder {
privateString id;
private Customer customer;
private List<OrderItem> orderItems;
private LogisticsOrder logisticsOrder;
}
-
在實體之上單獨定義一個聚合物件,在其中選擇一個實體作為聚合根。
classTradeOrderAggreagte{
private TradeOrder tradeOrder;
private Customer customer;
private List<OrderItem> orderItems;
}
兩種方式都實踐過後,我暫時傾向於第二種寫法,第一種方式實體和聚合的概念經常容易攪在一起,只需要關注實體本身時,又不得不去考慮這個物件中關聯的其他實體。第二種方式雖然命名會很冗長,但能夠保證實體間的關聯最大程度的減少,所有基於業務場景建立起來的關聯都集中在聚合內。兩種方式各有利弊,如果團隊在實踐的過程中能夠結合起來使用是最好的,還是那句話,軟體工程沒有“銀彈”,模型需要在實踐中不斷的演進和迭代,從簡單到複雜,只要我們時刻關注模型是否能夠反映業務實際情況。(各位看官如果有更好的方式也歡迎一起討論)下面我們來看一下完整的領域模型:
package entity;
class TradeOrder {
privateString id;
privateString customerId;
private Address address;
private BigDecimal ammount;
private Status status;
}
class OrderItem {
privateString id;
private Order order;
private Status status;
private Product product;
private int quantity;
private BigDecimal amount;
}
class Customer {
privateString id;
private Address address;
}
class LogisticsOrder {
privateString id;
private Address address;
private LogisticsStatus status;
}
class Product {
privateString id;
privateString name;
private BigDecimal price;
}
package aggregate;
classTraderOrderAggregate{
private TradeOrder tradeOrder;
private Customer customer;
private LogisticsOrder logisticsOrder;
private List<OrderItem> orderItems;
}
classLogisticsOrderAggregate{
private LogisticsOrder logisticsOrder;
private TradeOrder tradeOrder;
}
classCustomerOrderAggregate{
private Customer customer;
private List<TradeOrder> order;
}
classProductOrderAggregate{
private Product product;
private List<OrderItem> orderItems;
}
6. 查詢不是領域模型
需要強調的是,不要因為對資料的查詢需求而改變領域模型,領域模型是為了對映業務活動,以及業務活動的影響,這個影響可能是領域內的資料,也可能是對領域外的改變。在我們的開發過程中,頁面的展示,對外提供查詢介面往往是高頻變更的地方,查詢的邏輯也經常是無花八門,很難控制使用者想要把哪些資料聚合在一起展示。因此對於這種純查詢的場景,我們不要用領域模型去承載,最簡單直接的方式就是直接從資料層去查詢、拼裝資料。這也是命令查詢的責任分離(Command Query Responsibility Segregation,CQRS)這種設計模式一種體現。

在資料查詢中也會遇到一些資料庫物件有密切的聯絡,在多個場景中需要一起查出來,這個時候則可以透過構建一些讀模型來封裝查詢邏輯。原則上只要物件間都透過組合的方式來進行組裝,避免耦合,讀模型可以隨時按需來建立,不要吝嗇於建立一個物件。
為了避免誤解,這裡還是要講清楚的一點是,以上所說的查詢,和我們在寫鏈路裡需要從資料庫中重建領域物件,是兩種不同的場景。重建領域物件一般是透過repository來提供查詢介面,返回的結果一定是領域物件,重建出來的領域物件也一定是在寫入鏈路使用的。團隊以前也有過一個應用,無論是查詢還是寫入都要透過領域模型,導致各種複雜的查詢的邏輯是Repository逐漸膨脹,同時領域物件中也多了很多預期以外的屬性,模型從資料庫物件(DO)轉成領域物件(DomainObject)再轉成資料傳輸物件(DTO),對開發十分不友好。
領域物件的生命週期

前文我們在講實體的時候,簡單介紹了一個實體的生命週期,領域驅動設計為我們提供了一系列可選的構造塊,幫助我們將領域物件生命週期的各個環節需要關注的問題做進一步的分離。
1. 工廠(Factory)

不同於設計模式中的工廠模式,這裡的Factory僅僅是為了將領域物件建立的過程透過一種單獨的模式獨立出來。我們的一個系統,可能會對外提供多種型別、多種模式的入口,比如訊息監聽、端面、介面、定時任務等,不同的入口我們對外的契約不同,使用者能提供的入參也不相同。我們使用領域驅動設計來作為程式碼設計的基本訴求是所有的核心業務程式碼都基於領域物件,因此領域物件的建立是一切業務程式碼的開始。簡單點來說,Factory是承載將系統對外提供的請求模型轉換為領域模型功能的一系列物件,它包含兩個核心約束:
-
滿足客戶約束
-
滿足內部規則
classOrderServiceImpl{
private OrderApplicationService applicationService;
publiccreateOrder(OrderCreateRequestDTO request){
TradeOrderAggregate order = new TradeOrderAggregateFactory().buildOrder(request);
applicationService.createOrder(orderAggregate);
}
}
classOrderController{
publiccreateOrder(OrderCreateRequestVO request){
TradeOrderAggregate order = new TradeOrderAggregateFactory().buildOrder(request);
applicationService.createOrder(orderAggregate);
}
}
classTradeOrderAggregateFactory{
public TradeOrderAggregate buildOrder(OrderCreateRequestDTO request){
Assert.notNull(request);
return TradeOrderAggregate.builder()
.tradeOrder(new OrderFactory().build(request))
.orderItems(new OrderItemFactory().build(request))
.build();
}
public TradeOrderAggregate buildOrder(OrderCreateRequestDTO request){
Assert.notNull(request);
return TradeOrderAggregate.builder()
.tradeOrder(new OrderFactory().build(request))
.orderItems(new OrderItemFactory().build(request))
.build();
}
}
構建領域物件的Factory和領域物件的程式碼分層可以保持一致,聚合引用多個實體,聚合的Factory也可以飲用其他實體的Factory。只要領域物件之間的耦合度足夠低,基於領域物件的其他程式碼構造塊也可以保持低耦合高內聚。
2. 倉庫(Repository)

Repository提供了領域物件重建和持久化的功能,它隔離了領域模型與資料庫系統的複雜性,使開發人員可以將關注點分離開,在處理業務邏輯的時候,不需要考慮資料庫實現的問題;而當需要關注資料庫時,則關注於資料庫、ORM框架就可以。團隊在實踐的過程中將領域層(Domain)與資料接收層(DAL)做了依賴倒置,領域層僅依賴Repository的介面,具體實現在DAL層中實現,這樣即使未來換了資料庫實現或其他的基礎設施,對領域層的程式碼都是無需修改的。
publicclassTradeOrderAggregateReposiotry{
voidsave(TradeOrderAggregate tradeOrderAggregate);
}
publicclassTradeOrderAggregateReposiotryTddlImplimplementsTradeOrderAggregateReposiotry{
private TradeOrderRepository tradeOrderRepository;
private OrderItemRepository orderItemRepository;
voidsave(TradeOrderAggregate tradeOrderAggregate){
tradeOrderRepository.save(tradeOrderAggregate.getTradeOrder());
tradeOrderAggregate.getOrderItems().forEach(orderItem->
orderItemRepository.save(orderItem);
);
}
voidget(String tradeOrderId){
TradeOrder order = tradeOrderRepository.get(tradeOrderId);
List<OrderItem> orderItems = orderItemRepository.queryByOrderId(tradeOrderId);
return TradeOrderAggregate.builder()
.traderOrder(order)
.orderItems(orderItems)
.build();
}
}
3. 領域服務(Service)

相信我們工作的程式碼庫中最多類名字尾就是Service了,我們也應該被各種Service的呼叫層級、迴圈依賴教訓過很多遍了,出現這種問題實際上還是我們對程式碼缺少設計,一股腦的把業務邏輯、系統邏輯、應用邏輯、基礎設施等等隨意組裝到一起使用,本來每一個部分的複雜度就已經非常高了,我們還要將這些複雜度揉到一起。領域驅動設計給我們提供了一種分層治理的思路,將系統內的服務類分為幾個大類:應用層服務、領域層服務、基礎設施層服務。應用層服務用於處理輸入輸出、與領域模型和領域服務之間的排程、連線基礎設施層服務。
當領域模型中某個動作或者操作不能看作某個領域物件自身的職責時,可以將其託管到一個單獨的服務類中,這種服務類,我們把它叫做領域服務。對於領域服務的使用,經常很難去定義哪些行為或邏輯是應該託管到服務類中還是由領域物件自己來負責。全部託管到領域服務中,領域物件則會變成貧血模型,如果不託管,又容易因職責過多而導致領域物件過於膨脹。對於這個問題我們也沒有太好的解決辦法,軟體工程的問題永遠都是在Balance的過程中,當代碼複雜度可控的範圍內,我們儘量減少對領域服務的使用,如果領域物件開始出現膨脹的現象,那就將其託管到領域服務中。
對於領域服務,一定要把守住一條底線,領域服務一定不要有狀態,也就是我們所說的“純函式”,這樣做能夠讓領域服務保持單純,僅關注於領域物件之間的關係和其狀態的變化,而不會引入領域邏輯以外的複雜性。
領域模型嵌入工程
呼~領域驅動設計裡基本的構造單元已經介紹完了,接下來看看怎麼將這些單元融合在一起,使其成為一個可工作的軟體。這部分在《領域驅動設計》裡作者Eric Evans將其稱為分離領域,對它的介紹放在最開始的部分,我換了一個思路將它放在了最後,並換了個方向從分離的視角換成嵌入的視角。如果我們不做工程,只是簡單的寫一個程式,我們都可以很熟練的使用面向物件,但就是因為工程的複雜性,導致我們沒有辦法隨心所欲去用面向物件裡的各種優秀設計。
假設你現在有一個完整領域模型的二方包,裡面完全由上述所有的程式碼構造塊組成,不依賴資料庫、環境、框架外部系統等等,接下來只需要把這個核引入到我們的工程程式碼中,完成它與實際應用的關聯。Eric Evans為領域驅動設計提供了一個分層架構,使用者介面層 – 應用層 – 領域層 – 基礎設施層,後來也有人提出了洋蔥架構和六邊形架構等,它們都有一個共同特徵:獨立且處於核心的領域層。對於這幾種架構的介紹,網上有很詳細的資料,我這裡不展開進行介紹,蒐羅幾張圖供大家瞭解:




可以看到,在將領域模型與工程結合的過程中,應用服務(ApplicationService)扮演了十分重要的角色,它對入口、領域模型、外部依賴、基礎設施等部分進行編排和排程,最終使領域模型能夠在實際應用中正常工作。
classTradeOrderApplicationService{
voidcreateTradeOrder(TradeOrderAggregate tradeOrderAggregate){
// 從領域外獲取客戶資訊,對映到當前上下文
Customer customer = customerFacade.getCustomer(tradeOrderAggregate.getCustomerId());
// 宣告式設計,顯性表達領域物件在特定場景中的規約
new TradeOrderSpecification(tradeOrderAggregate).isSatisfiedCreate(customer);
// 呼叫領域物件方法完成領域物件狀態的改變,如果邏輯逐漸複雜超出領域物件職責範圍,可以託管到領域服務中
tradeOrderAggregate.created();
// 使用repository持久化物件
tradeOrderAggregateRepository.save(tradeOrderAggregate);
// 排程其他系統、基礎設施中介軟體等
msgService.send(tradeOrderAggregate);
}
}
以下是我們團隊當前正在使用的一種分層模式,基本上前面也都介紹的差不多了,貼一下我們一個工程的程式碼分層目錄吧:

💡 在現在微服務氾濫的現狀下,為了每個領域能夠自治,往往領域會拆分的很細,領域間為了防止耦合過深,一般會選擇建立起高高的邊界,導致領域間上下文對映會越來越複雜,領域內也會有越來越多的防腐層建設。深度自治過後帶來的理解成本和維護成本都呈指數級上升。有沒有可能在一定範圍內的團隊能夠共同維護一套領域模型,這個模型透過二方包版本升級來更新,各個團隊基於領域模型來完成應用層和基礎設施層的建設,透過這種方式減少因人而產生的認知成本以及協同成本,同時它也不違背微服務的理念。我剛來阿里時是在供應鏈中臺,當時我所在團隊的前身是盒馬供應鏈,我從程式碼中看到之前的架構師輝子老師似乎做過這種嘗試,將業務的變化表現在領域模型中,架構師只需要關注核心領域模型的變化,而不用過於關注團隊的技術架構和系統架構建設。但當時也只是侷限於統一了領域模型的屬性,沒有定義行為,而且我加入團隊的時候,輝子老師已經離開了,這種約束也不復存在,加上業務和組織的變化,最後還是把模型分散到各個團隊自治了。如果對這種方式有興趣的同學,可以評論區一起討論討論利弊以及可操作性。