C#進階系列——DDD領域驅動設計初探(一):聚合


前言:又有差不多半個月沒寫點什么了,感覺這樣很對不起自己似的。今天看到一篇博文里面寫道:越是忙人越有時間寫博客。呵呵,似乎有點道理,博主為了證明自己也是忙人,這不就來學習下DDD這么一個聽上去高大上的東西。前面介紹了下MEF和AOP的相關知識,后面打算分享Automapper、倉儲模式、WCF等東西的,可是每次准備動手寫點什么的時候,就被要寫的Demo難住了,比如倉儲模式,使用過它的朋友應該知道,如果你的項目不是按照DDD的架構而引入倉儲的設計,那么會讓它變得很“雞肋”,用不好就會十分痛苦,之前看過這篇 博客園的大牛們,被你們害慘了,Entity Framework從來都不需要去寫Repository設計模式 文章的朋友應該還記得,不止是該文章的作者,很多園友在評論里面也提到了使用它的不爽。博主的項目中也遇到類似的問題,雖然引入了倉儲模式,但是由於沒有架構好,把倉儲的接口和實現統一放在了數據訪問層,導致到后面代碼越寫越難維護,完全感覺不到倉儲帶來的好處。所以博主覺得單純分享倉儲模式很容易使讀者陷入“為了模式而模式”的誤區,再加上最近一段時間在看《領域驅動設計:軟件核心復雜性應對之道.Eric.Eva》這本書和博客園大牛dax.net的DDD系列文章,所以打算分享一個Demo來說明倉儲模式、Automapper、WCF等知識點。

DDD領域驅動設計初探系列文章:

一、領域驅動設計基本概念

根據《領域驅動設計:軟件核心復雜性應對之道.Eric.Eva》書中的觀點,領域模型是軟件項目的公共語言的核心,是領域專家和開發人員共同遵守的通用語言規則,那么在DDD里面,建模的重要性不用多說,所以要想更好理解領域驅動設計,理解領域模型的划分和建立就變得相當必要。首先來看看DDD里面幾個比較重要的概念:

1、領域模型:領域模型與數據模型不同,它表述的是領域中各個類及其之間的關系。從領域驅動設計的角度看,數據庫只不過是存儲實體的一個外部機制,是屬於技術層面的東西。數據模型主要用於描述領域模型對象的持久化方式,應該是先有領域模型,才有數據模型,領域模型需要通過某種映射而產生相應的數據模型,從這點來說,最新的EF的Code First就是一個很好的體現。領域模型對象分為實體、值對象和服務。

2、實體:在領域驅動設計里面,實體是模型中需要區分個體的對象。這里的實體和EntityFramework里面的實體不是一個概念,EF的實體為數據實體,不包含對象的行為。就博主的理解,DDD概念里面的實體就是包括實體數據(EF的Model)和行為的結合體

3、值對象:通過對象屬性值來識別的對象,它將多個相關屬性組合為一個概念整體。相比實體而言,值對象僅僅是比實體少了一個標識。值對象的設計比較存在爭議,我們暫且記住值對象和實體的區別:(1)實體擁有唯一標識,而值對象沒有;(2)實體允許變化,而值對象不允許變化;(3)判斷兩個實體相等的方法是判斷實體的標識相等,而判斷兩個值對象相等的標准是值對象內部所有屬性值相等;

4、聚合(以及聚合根):聚合表示一組領域對象(包括實體和值對象),用來表述一個完整的領域概念。而每個聚合都有一個根實體,這個根實體又叫做聚合根。舉個簡單的例子,一個電腦包含硬盤、CPU、內存條等,這一個組合就是一個聚合,而電腦就是這個組合的聚合根。博主覺得關於聚合的划分學問還是挺大的,需要在實踐中慢慢積累。同一個實體,在不同的聚合中,它可能是聚合根,也可能不是,需要根據實際的業務決定。聚合根是聚合所表述的領域概念的主體,外部對象需要訪問聚合內的實體時,只能通過聚合根進行訪問,而不能直接訪問

5、領域服務:博主的理解,領域模型主張富領域模式,也就是說把領域邏輯盡量寫在領域實體里面,也就是常說的“充血模式”,而對於業務邏輯,最好是以服務的形式提供。至於領域邏輯和業務邏輯的界定,這個要根據實際情況來定。總之,領域服務是用來處理那些領域模型里面不好定義或者某些可變邏輯的的時候才會用到。待驗證!

6、工廠、倉儲等概念留在Demo里面說明。

 

二、領域驅動設計開始之旅

1、項目分層

領域驅動設計將軟件系統分為四層:基礎結構層、領域層、應用層和表現層。來看看書中的分層:

其實在dax.net的系列中這張圖更能說明這種架構

 

2、項目架構

博主打算用權限系統的案例說明的領域驅動設計的項目架構。項目嚴格按照表現層、應用層、領域層、基礎設施層來划分。

表現層:MVC的Web項目,負責UI呈現。

應用層:WCF服務,負責協調領域層的調用,向UI層提供需要的接口。

領域層:定義領域實體和領域邏輯。

基礎設施層:一些通用的技術,比如AOP、MEF注入、通用的工具類、DTO模型層,這里為什么要有一個DTO模型層,DTO是用於UI展現用的純數據Model,它不包含實體行為,是一種貧血的模型。

整個項目的調用方式嚴格按照DDD設計來進行,UI層通過WCF服務調用應用層的WCF接口,WCF服務通過倉儲調用領域層里面的接口,基礎設施層貫穿其他各層,在需要的項目中都可以引用基礎設施層里面的內庫。

 

3、代碼示例

接下來,博主就根據自己的理解,從零開始使用這種架構寫一個簡單的權限管理系統。由於是領域驅動設計,所以,文章的重點會放在領域層,項目使用了EF的Model First來進行,先設計實體,后生成數據庫。

3.1 首先來看看表結構

 

根據博友要求,這里說明一下表之間的映射關系:

1表示TB_DEPARTMENT表的主鍵DEPARTMENT_ID作為TB_USERS表的外鍵;

2表示TB_USERS表的主鍵USER_ID作為TB_USERROLE表的外鍵;

3表示TB_ROLE表的主鍵ROLE_ID作為TB_USERROLE表的外鍵;

4表示TB_ROLE表的主鍵ROLE_ID作為TB_MENUROLE表的外鍵

5表示TB_MENU表的主鍵MENU_ID作為TB_MENUROLE表的外鍵

首先建好對應的表實體,然后根據模型生成數據庫

將生成的sql語句執行后就可以得到對應的表結構。

 

3.2 聚合的划分

在領域層里面我們新建一個BaseModel,里面有三個類

這三個類IEntity、IAggregateRoot、AggregateRoot分別定義了實體的接口、聚合根的接口、聚合根的抽象實現類。

    //用作泛型約束,表示繼承自該接口的為領域實體
    public interface IEntity
    {

    }
    /// <summary>
    /// 聚合根接口,用作泛型約束,約束領域實體為聚合根,表示實現了該接口的為聚合根實例,由於聚合根也是領域實體的一種,所以也要實現IEntity接口
    /// </summary>
    public interface IAggregateRoot:IEntity
    {

    }
   /// <summary>
    /// 聚合根的抽象實現類,定義聚合根的公共屬性和行為
    /// </summary>
    public abstract class AggregateRoot:IAggregateRoot
    {
        
    }

這里定義接口的作用是定義實體和聚合根的泛型約束,抽象類用來定義聚合根的公共行為,目前為止,這些接口和類里面都是空的,后面會根據項目的需求一步一步往里面加入邏輯。

在EF里面由edmx文件會生成實體的屬性,前面說到領域模型主張充血模式,所以要在EF的實體model里面加入實體的行為,為了不改變EF生成實體的代碼,我們使用partial類來定義實體的行為。我們來看Model文件夾下面的代碼

    public partial class TB_DEPARTMENT: AggregateRoot
    {
        public override string ToString()
        {
            return base.ToString();
        }
    }
    public partial class TB_MENU : AggregateRoot
    {

    }
    /// <summary>
    /// 由於不會直接操作此表,所以TB_MENUROLE實體不必作為聚合根,只是作為領域實體即可
    /// </summary>
    public partial class TB_MENUROLE:IEntity
    {
    }
    public partial class TB_ROLE:AggregateRoot
    {
    }
    public partial class TB_USERROLE:IEntity
    {
    }
    public partial class TB_USERS:AggregateRoot
    {
    }

我們看到,這些實體,只有TB_MENUROLE和TB_USERROLE不是聚合根,其他實體都是聚合根。我這里大概划分為4個聚合:

聚合1:TB_DEPARTMENT實體

聚合2:TB_MENU、TB_MENUROLE、TB_ROLE這3個為一個聚合,聚合根是TB_MENU。

聚合3:TB_USERS、TB_USERROLE、TB_DEPARTMENT、TB_ROLE這4個為一個聚合,聚合根是TB_USERS。

聚合4:TB_ROLE、TB_USERS、TB_USERROLE、TB_MENUROLE、TB_MENU這5個表為一個聚合,聚合根是TB_ROLE。

可能這樣分會有一定的問題,后續出現再慢慢糾正。

到這里,聚合的划分基本完了,至於為什么要做這么一些約束和設計,因為倉儲只能對聚合根做操作,下篇講倉儲的時候會詳細說明。

DDD博大精深,文中很多觀點為博主個人理解,可能不太成熟或者有誤,歡迎拍磚~~

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com