領域驅動設計基本概念答疑

本文內容來自我的知識星球「逸派胡言」對群友提出的有關領域驅動設計基本概念的回答。
實體與值物件
問題:DDD實現中領域物件區分實體(Entity)和值物件(Value Object)的目的(Why)是什麼?或者換一種問法:領域物件區分實體(Entity)和值物件(Value Object)之後,帶來的好處和收益是什麼?
回答:從DDD的概念上講,實體(Entity)與值物件(Value Object)的本質區別僅在於後者無需identity(唯一標識)。這其實就是帶來的價值——就是你設計的物件不需要去跟蹤和管理這個唯一標識。
這是概念劃分上,值物件帶來的價值。
再來說設計層面。通常情況下,我們建議將值物件設計成一個不變(Immutable)物件。當一個物件是不變的時,你就基本不需要擔心併發帶來的諸如同步、衝突等問題了,這既降低了程式設計的難度,又可以無需引入額外的同步鎖影響程式的效能。
反而過來說,之所以可以將值物件設計成不變的,其根本原因還是在於我們無需跟蹤和管理唯一標識。
在領域驅動設計中,我們提倡的實踐是儘量定義值物件來替代基本型別,原因在於基本型別無法體現統一語言中的領域概念。此外,在多數語言中,我們無法對基本型別做封裝,就意味著一個領域概念缺乏領域行為來支援。假設一個實體定義了許多屬性,如果這些屬性都是基本型別,就會導致與這些屬性相關的領域行為都要放到實體中,導致實體的職責變得不夠單一。
引入值物件後,情況就不同了,因為我們可以利用合理的職責分配,將這些職責(領域行為)按照內聚性分配到各個值物件中,這個領域模型就能變得協作良好。
當然,反過來說,之所以可以這樣設計,還是在於值物件無需承擔跟蹤和管理唯一標識的職責。
這也是為何Eric要將實體和值物件分開的主要原因,也是值物件給我們帶來的價值所在。
Repository與DAO
問題:Repository與DAO其實都是兩種模式的名稱。然而在領域驅動設計中,名稱本身就是非常重要的。Dao即Data Access Object,即資料訪問物件。從其命名上看,就應該屬於資料訪問層,即DDD中的基礎設施層。
回答:在DDD中,所有的領域物件應該都屬於領域層。那麼,該如何訪問這些領域物件呢?DDD希望解除領域層與基礎設施層之間的關係,即將設計的注意力完全放在領域建模和領域設計上,思考領域邏輯的實現時,應儘可能地不要考慮領域物件的持久化(資料訪問),於是就定義了Repository這個抽象。無論放在哪裡(檔案、DB或者記憶體),Repository都將其視為一個“資源庫”的抽象。經過這麼一層的抽象之後,獲取領域物件,或者說管理領域物件生命週期的邏輯就應該屬於領域層。
在實現上,你當然可以將這樣的Repository介面命名為DAO,這本身沒有問題,但名不正則言不順,如果在領域層中夾雜了一個名為DAO的介面,仍然有“將基礎設施混入領域層”的嫌疑。
所以,Repository是抽象,代表了對領域物件生命週期的管理,但並不等於是持久化,持久化只是Repository的其中一種實現。你可以假設一臺伺服器無比的強大,記憶體大且永遠不會宕機,這時何須持久化呢?但無論怎麼修改生命週期的具體管理方式,都不會影響到Repository的抽象。
領域服務與應用服務
問題:應用服務與領域服務的區別在哪?
回答:從分層架構上,應用服務屬於應用層,領域服務屬於領域層。
從職責上看,應用服務只是一個門面(Facade),它具體並不做領域服務的活兒,也就是不提供領域實現,也就是不包含業務邏輯。之所以要引入應用服務,有兩個原因:
領域服務或其他領域物件的粒度太細(便於協作、擴充套件和重用),不利於客戶端的呼叫,基於“最小知識原則”,還是讓客戶端少知道這些領域物件協作的知識為好。此時的應用服務更像是對領域物件的一種“編排”。
在呼叫領域物件去完成一個用例時,不可避免地要牽涉到一些屬於“橫切關注點”的內容,如事務、異常處理、授權認證等。這些橫切關注點從職責上看,不屬於領域層,放在領域服務中可能會導致對領域邏輯的汙染,這些職責就像砌磚牆時需要的水泥。水泥自身不提供磚頭的職責,但沒有水泥,牆就沒法砌起來。
具體對領域服務和應用服務的闡述,可以看我的這篇文章《 一篇文章教你分辨應用服務和領域服務 》。
請關注我的「知識星球」:
