- 關聯物件
- 無法封裝的資料庫開銷
- 引入關聯物件
- 背景關系過載
- 因富含邏輯而產生的過大類
- 邏輯匯聚于背景關系還是物體
- 通過角色物件分離不同背景關系的邏輯
- 通過背景關系物件分離不同背景關系的邏輯
- 架構分層
- DDD中的分層的問題
- 基礎設施層與領域層誰更穩定
- 基礎設施不是層
- 能力供應商模式
- 從基礎設施到有業務含義的能力
- 將技術組件進行擬人化處理
- 使用能力供應商的多層架構
- 能力供應商模式的缺點
- 從基礎設施到有業務含義的能力
- DDD中的分層的問題

在落地DDD時,關聯模型與軟體實作總有讓人糾結與苦惱的地方,引起這些苦惱的的主要原因是架構風格的變化,我們已經從多層單體架構時代,過渡到了云原生分布式架構,但所采用的建模思路與編程風格并沒有徹底跟上時代的步伐,這種差異通常會以性能問題或是代碼壞味道的形式出現,
想要真正發揮出DDD的作用,就需要在不同架構風格下,找到能夠維持模型與軟體實作統一的辦法,
關聯物件
DDD中的聚合關系在具體實作中會存在一些問題,
無法封裝的資料庫開銷
聚合與聚合根是構成"富含知識的模型"的關鍵,通過聚合關系,可以將被聚合物件的集合邏輯放置在聚合或聚合根,而不是散落在外,或是放在其它無關的服務中,以避免邏輯泄露,
但在落地時,經常會遇到一個挑戰,即:這些被聚合的物件,通常都是被資料庫持久化的集合,對資料庫系統操作無法被介面抽象隔離,而將技術實作引入領域模型,則有悖領域驅動設計的理念,
比如在極客時間的例子里,要對用戶已經訂閱過的專欄進行分頁顯示,因為性能原因,不能將DB中的Subscription資料全部讀取到記憶體中再進行分頁,而要在查詢DB時包含分頁邏輯,
那么分頁邏輯放在哪里,才能保持模型與軟體實作的關聯呢?
- 一種做法是為Subscription構造一個獨立的Repository物件,將分頁邏輯放在里面,但這種做法會導致邏輯泄露,因為Subscription被User聚合,那么User所擁有的Subscription的集合邏輯應該被封裝在User中,為非聚合根提供Repository是一種壞味道,
- 那么把分頁邏輯放到User上呢,這樣其實也不合適,因為這樣會將技術實作細節引入領域邏輯中,無法保持領域邏輯的獨立,
造成上面兩難局面的根源在于:我們希望在模型中使用集合介面,并借助它封裝具體實作的細節;這基于一個前提,即記憶體中的集合與資料庫是等價的,都可以通過集合介面封裝,但實際情況是,我們無法忽略資料庫帶來的額外開銷,兩者并不等價,
引入關聯物件
關聯物件是將物件間的關聯關系直接建模出來,然后再通過介面與抽象的隔離,把具體技術實作細節封裝到介面的實作中,
User與Subscription間存在關聯關系,所以新增一個關聯物件來表達:
public interface IMySubscriptions : IQueryable<Subscription>
{
...
}
public class User
{
private IMySubscriptions _mySubscriptions;
}
這里的關聯物件為IMySubscriptions,User是Subscription的聚合根,與之相關的邏輯通過_mySubscriptions完成,仍然在User的背景關系中,沒有邏輯泄露,
然后,再通過介面與實作分離的方式,從領域物件中移除對具體技術實作的依賴,通過依賴注入的方式提供介面的具體實作:
public class MySubscriptionsDB : IMySubscriptions
或者資料來源于Restful API
public class MySubscriptionsAPI : IMySubscriptions
關聯物件實際上是通過將隱式的概念顯式化建模來解決問題的,這是面向物件技術解決問題的通則:永遠可以通過引入另一個物件解決問題,
背景關系過載
背景關系過載(Context Overloading)就是指領域模型中的某個物件會在多個背景關系中發揮重要作用,甚至是聚合根,這會導致物件本身變得很復雜、模型僵化;還可能帶來潛在的性能問題,
因富含邏輯而產生的過大類
假設之前的極客時間例子中,模型經過擴展后,包含了三個背景關系:
- 訂閱:用戶閱讀訂閱內容的背景關系,根據訂閱關系判斷哪些內容是用戶可見的;
- 社交:用戶維持朋友關系的背景關系,可以分享動態與資訊;
- 訂單:用戶購買專欄的背景關系,通過訂單與支付,完成對專欄的訂閱,
按照這個模型,得到的富含知識的實作為:
public class User
{
// 社交背景關系
private List<Friendship> _friendships;
public void Make(Friendship friendship)
{
}
// 訂閱背景關系
private List<Subscription> _subscriptions;
public void Subscribe(Subscription subscription)
{
}
// 訂單背景關系
private List<Order> _orders;
public void PlaceOrder(Order order)
{
}
}
這個實作的問題在于一個物件包含了不同的背景關系,即壞味道:過大類,壞處有
- 模型僵硬,想要理解這個類的行為,就必須理解所有的背景關系,只有理解了背景關系,才能判斷其中的代碼和行為是否合理,于是背景關系的過載就變成了認知的過載,而認知的過載又會造成維護的困難,出現“看不懂、改不動”的祖傳代碼,而改不動的代碼就是改不動的模型,最終提煉知識的回圈也就無法進行了;
- 過大類還容易滋生重復代碼、引入偶然耦合造成的意外缺陷;
- 性能問題,在不同的背景關系中,需要訪問的資料也不盡相同(這個問題可以通過引入關聯物件緩解)
邏輯匯聚于背景關系還是物體
背景關系過載的根本癥結在于:邏輯匯聚于背景關系還是物體,
DDD的默認風格是匯聚于物體,類似這里的User類;而如果根據DCI范型(Data-Context-Interaction,資料-背景關系-互動),則應該匯聚于顯式建模的背景關系物件(Context Object)中,或者背景關系中的角色物件(Role Object)中,
這樣做的原因是因為,在不同的背景關系中,用戶是以不同的角色與其他物件發生互動的,User在訂閱背景關系中的角色是Reader,在訂單背景關系中是Buyer,在社交背景關系中則是Contact,
而發生背景關系過載的根源為:物體在不同的背景關系中扮演的多個角色,再借由聚合關系,將不同背景關系的邏輯富集于物體中,導致了背景關系過載,
所以解決方案就是:針對不同背景關系的角色建模,將對應的邏輯富集到角色物件中,再讓物體物件去扮演不同的角色,
通過角色物件分離不同背景關系的邏輯
一種實作思路是通過裝飾器模式,構造一系列角色物件(Role Object)作為User的裝飾器:
public class Buyer
{
private User _user;
private List<Order> _orders;
public Buyer(User user)
{
_user = user;
}
public void PlaceOrder(Order order)
{
}
}
public class Reader
{
private User _user;
private List<Subscription> _subscriptions;
public Reader(User user)
{
_user = user;
}
public void Subscribe(Subscription subscription)
{
}
}
...
在具體的Repository實作中使用這些角色物件:
public class UserRepositoryDB : IUserRepository
{
public User FindById(long id)
{
return db.ExecuteQuery(...);
}
public Buyer AsBuyer(User user)
{
return new Buyer(user, db.ExecuteQuery(...));
}
public Reader AsReader(User user)
{
return new Reader(user, db.ExecuteQuery(...));
}
}
之后,就可以類似下面這樣獲取角色物件了:
var user = repo.FindById(1);
var buyer = repo.AsBuyer(user);
var reader = repo.AsReader(user);
使用角色物件的好處:
- 把不同背景關系中的邏輯分別富集于不同的角色物件中;解決了認知過載的問題,同時也通過封裝隔離了不同背景關系的變化,
- 從物體物件轉化到角色物件經由了顯式的方法呼叫,這實際上清晰地表示了背景關系的切換,
但這個方案在揭示意圖、技術解耦上還做得不夠好;比如假設不是所有資料都來自資料庫,社交背景關系中的朋友關系來自Restful API呼叫,這種情況下,將AsContact放到UserRepositoryDB就不合適了,
通過背景關系物件分離不同背景關系的邏輯
既然將角色轉換的邏輯放到UserRepositoryDB不合適,那么借鑒前面關聯物件的思路,將背景關系直接建模出來,并通過介面隔離具體實作:
public interface IOrderContext
{
interface IBuyer
{
void PlaceOrder(Order order);
}
IBuyer AsBuyer(User user);
}
public interface ISocialContext
{
interface IContact
{
void Make(Friendship friendship);
}
IContact AsContact(User user);
}
public interface ISubscriptionContext
{
interface IReader
{
void Subscribe(Subscription subscription);
}
IReader AsReader(User user);
}
然后將背景關系物件的獲取放置到IUserRepository介面中,并在其實作中使用依賴注入獲取不同的背景關系物件:
public interface IUserRepository
{
User FindUserById(long id);
ISubscriptionContext InSubscriptionContext();
ISocialContext InSocialContext();
IOrderContext InOrderContext();
}
public class UserRepositoryDB: IUserRepository
{
//通過依賴注入獲取不同的背景關系物件
private ISubscriptionContext subscriptionContext;
private ISocialContext socialContext;
private IOrderContext orderContext;
....
}
最后的使用方式就成了:
var buyer = repo.InOrderContext().AsBuyer(user);
var reader = repo.InSubscriptionContext().AsReader(user);
var contact = repo.InSocialContext().AsContact(user);
使用背景關系物件重構后得到的好處有:
- 借由背景關系的封裝,不同背景關系中的技術實作可以是異構的,不管資料來自資料庫還是第三方API,這些細節都不會暴露給使用者;
- 軟體實作、模型、統一語言更加緊密地關聯在了一起,背景關系物件與界限背景關系對應,
- 更加清楚地揭示了領域知識的意圖,如下圖的領域模型:

通過如下IUserRepository的定義可知,User在三個不同的背景關系中扮演不同的角色,
public interface IUserRepository
{
User FindUserById(long id);
ISubscriptionContext InSubscriptionContext();
ISocialContext InSocialContext();
IOrderContext InOrderContext();
}
架構分層
如何組織領域邏輯與非領域邏輯,才能避免非領域邏輯對模型的污染,通常會使用分層架構來區分不同的邏輯,將不同的關注度的邏輯封裝到不同的層中,以便擴展維護,同時也能有效地控制變化的傳播,
不同層有不同的需求變化速率(Pace of changing),分層架構對變化傳播的控制,是通過層與層之間的依賴關系實作的,因為下層的修改會波及到上層,所以希望通過層來控制變化的傳播,只要所有層都單向依賴比自己更穩定的層,那么變化就不會擴散了,
DDD中的分層的問題
在DDD中通常會將系統分為四層:

- 展現層(Representation Layer),負責給最終用戶展現資訊,并接受用戶的輸入作為功能的觸發點,如果不是人機互動系統,用戶也可以是其他軟體系統,
- 應用層(Application Layer),負責支撐具體的業務或者互動流程,將業務邏輯組織為軟體的功能,
- 領域層(Domain Layer),核心的領域概念、資訊與規則,它不隨應用層的流程、展現層的界面以及基礎設施層的能力改變而改變,
- 基礎設施層(Infrastructure Layer),通用的技術能力,比如資料庫、MQ等,
基礎設施層與領域層誰更穩定
在上圖的四層架構中
- 展現層最容易改變:新的互動模式、不同的視覺模板都會導致改變;
- 應用層的邏輯會隨著業務流程以及功能點的變化而改變,比如流程的重組與優化、新功能點的引入;
- 領域層是核心領域概念的提取,理論上來說,如果通過知識消化完成模型的提取,那么由模型構成的領域層應該就是穩定態了,不會發生重大變化;
- 基礎設施層的邏輯由所選擇的技術堆疊決定,更改技術組件、替換框架都會造成基礎設施層的變化,基礎設施層的變化頻率與所用的技術組件有關,越是核心的組件,變化就越緩慢,比如相對資料庫,快取系統的變化頻率往往會更快,
此外,基礎設施層還可能發生不可預知的突變,比如過去的NoSQL、大資料、云計算都曾為基礎設施層帶來過突變,而且,周圍系統生態的演化與變更也會造成影響,比如訊息通知系統從短信變成微信,支付從網銀變成移動支付等等,
總之基礎設施層沒有領域層穩定,但上圖中,怎么能讓領域層依賴基礎設施層呢?
基礎設施不是層
領域模型對基礎設施的態度是非常微妙的,一方面,領域邏輯必須依賴基礎設施才能完成相應的功能,另一方面,領域模型必須強調自己的穩定性,才能維持它在架構中的核心位置,為了解決這個矛盾,要么承認領域層并不是最穩定的;要么就別把基礎設施當層看,
領域層被人為地設定為最穩定的,實際上可以將領域層看做“在特定技術堆疊上的領域模型實作”;但這樣可能無法被大多數DDD實踐者接受,所以剩下一個選擇:基礎設施不是層,
能力供應商模式
如何才能取消基礎設施層,但仍然不影響領域模型的實作呢,可以使用能力供應商(Capability Provider)模式,
從基礎設施到有業務含義的能力
假設極客時間的訂單需要通過網銀來支付,并通過郵件將訂單狀態發送給客戶,模型為:

偽代碼:
public class Order {
public void Pay(){
bank.pay(...);
email.send(...);
}
}
這樣的實作有個問題是領域層的Order直接依賴了基礎設施層的網銀支付、發郵件功能;而領域層是絕對穩定的,它不能依賴任何非領域邏輯(除了基礎庫),
怎么辦呢,需要將對基礎設施層的依賴,看做一種未被發現的領域概念進行提取,這樣其實就發揮了我們定義業務的權利,從業務角度去思考技術組件的含義,
將技術組件進行擬人化處理
通過擬人化,可以清楚地看到技術組件幫我們完成了什么業務操作,比如轉賬的時出納(Cashier),通知用戶的是客戶(Customer Service),于是模型就能轉化為:

這樣就可以將具有業務含義的能力抽象成介面納入領域層,而使用基礎設施的技術能力去實作領域層的介面,即基礎設施層成為了能力供應商,
雖然從實作上看,只是將對具體實作的依賴,轉化為對介面的依賴,但這樣做的好處卻契合了“兩關聯”:
- 領域模型與軟體實作關聯
- 統一語言與模型關聯
使用能力供應商的多層架構
可以將基礎設施看做對不同層的擴展或貢獻,它雖被介面隔離,卻是其它層的有機組成部分,作為能力供應商,參與層內、層間的互動,

能力供應商是一個元模式,關聯物件、角色物件、背景關系物件都可以看做它的具體應用,
能力供應商模式的缺點
能力供應商模式有一個缺點是將顯式的依賴關系,轉化為了隱式的依賴關系,這就對知識管理有了更高的要求,
這里把技術概念轉換成了領域概念,并反映到統一語言上,這就需要團隊不斷地執行回圈,才能把知識消化掉,業務方與技術方也需要緊密地配合與信任,
不要用解決方案去定義問題,而是問題定義解決方案,相同的解決方案,在面對不同的問題是就是不同的模式,比如代理模式 裝飾器模式 中介者模式,解決方案都是一個類代理給另一個類,但它們并不是同一個東西
參考資料
極客時間:如何落地業務建模 徐昊
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/413132.html
標籤:領域驅動設計
