本文將介紹DDD分層架構中廣泛使用的數據傳輸對象Dto,並且與領域實體Entity,查詢實體QueryObject,視圖實體ViewModel等幾種實體進行比較。
領域實體為何不能一統江湖?
當你閱讀我或其它博主提供的示例代碼時,會發現幾種類型的實體,這幾種實體初步看上去區別不大,只是名稱不同,特別在這些示例非常簡單的情況下更是如此。你可能會疑惑為何要搞得這么復雜,采用一種實體不是更好?
在最理想的情況下,我們只想采用領域實體Entity進行所有的操作。
領域實體是領域層的核心,是業務邏輯的主要放置場所。換句話說,領域實體中包含了大量業務邏輯方法。
領域實體在表現層進行模型綁定時可能遇到障礙
如果領域實體中的屬性都包含getter和setter,並且所有屬性都是public的,那么,使用這個Entity的程序員可能會繞過業務方法,直接操作屬性進行賦值。
為屬性直接賦值,是面向數據的過程式思維,而調用方法是面向對象的方式,這也是領域模型的核心所在。
所以為了強制實施業務規則,必須把業務方法操作過的屬性的setter訪問器隱藏起來,否則這個方法不會有人調用。
當領域實體某些屬性的setter被隱藏后,直接在表現層操作領域實體將變得困難,因為Mvc或Wpf的模型綁定只能操作public的屬性。
序列化領域實體可能遇到障礙
哪怕你的系統沒有使用分布式,比如只是一個Mvc網站,但由於前端要求越來越高,客戶端很多時候需要通過ajax與服務端進行交流,一般采用json格式傳遞數據,這就要求你的實體能夠序列化。
對領域實體進行序列化,首先需要考慮的問題是,可能序列化一個較大的對象圖,從而導致不必要的開銷。
領域實體一般包含導航屬性指向其它領域實體,其它的領域實體可能包含更多導航屬性,從而組成一個對象圖。如果采用Serializable特性進行序列化,並且沒有指定其它序列化選項,可能導致把一個龐大的對象圖序列化並進行網絡傳輸。
另一個問題是,復雜的領域實體可能包含循環引用,從而導致序列化失敗。
對於序列化,一個更好的選擇是采用DataContract特性,被DataContract修飾過的類成員,不會被自動序列化,必須在成員上明確指定DataMember特性。
DataMember在一定程度上可以緩解上述問題,比如減少需要序列化的數據,不序列化循環引用的對象等,但無法從根本上解決問題。
領域實體無法應對多客戶端應用需求
對於不同的客戶端,可能需要的數據和格式不同,這屬於應用層需求,而領域實體只有一個,在領域實體上通過標記DataMember進行序列化費力不討好,無法滿足復雜的應用需求。
哪怕你只有一個Mvc網站,如果頁面上需要顯示一些領域實體不存在的數據,你根據這個需求,直接在領域實體上增加屬性是非常糟糕的做法,會嚴重污染你的領域模型,將大大降低領域實體的復用能力。
從以上可以看出,對於一個比較復雜的系統,單憑領域實體很難完成任務,將太多的職責強加到領域實體上,會導致領域實體嚴重變形。
數據傳輸對象介紹
數據傳輸對象,即Data Transfer Object,簡稱DTO。
一個為了減少方法調用次數而在進程間傳輸數據的對象,《企業應用架構模式》如是說。
可以看出,DTO用於分布式環境,主要用來解決分布式調用的性能問題。同一進程內的對象調用,速度是非常快的,但跨進程調用,甚至跨網絡調用,性能下降N個數量級。為了提升性能,需要減少調用次數,這就要求把多次調用的結果打包成一個對象,在一次調用中返回盡量多的數據。
上面是DTO的原始含義,下面來看看我的山寨用法。
雖然我也取名為DTO,但我的動機並不完全是一次打包更多數據來提升性能,而是解決上面提到的幾個問題,當然它們之間有一定關系,可以看作一種變種用法。
DTO的長相
DTO是一個貧血對象,也就是它里面基本沒有方法,只有一堆屬性,並且所有屬性都具有public的getter和setter訪問器。
DTO擁有public的setter訪問器,方便的解決了表現層的模型綁定問題。
由於DTO不執行業務操作,僅用於傳遞數據,所以不應該定義非常復雜的對象引用關系,這樣就避免了循環引用,解決了對象序列化的問題。
DTO的粒度
DTO可以根據應用需求定義成不同的粒度,在一般情況下,DTO是聚合粒度,也就是說,一個領域層的聚合對應一個DTO,這樣做的一個好處是方便對CRUD操作進行抽象以及代碼生成。
界面如果想保持簡單,應該盡量一個界面操作一個聚合,將聚合的數據映射到DTO后,傳給視圖展示。
對於更加復雜的界面,需要在一個界面操作多個聚合,這種情況下,把需要的全部數據打包到DTO進行操作。
從以上介紹中,你應該了解DTO不能理解為單表操作,它可以包含你需要的全部數據。
DTO的位置
DTO處於應用層,在表現層與領域層之間傳遞數據。
DTO由應用層服務使用,應用層服務從倉儲中獲得聚合,並調用DTO轉換器將聚合映射為DTO,再將DTO傳遞給表現層。
關於應用層服務,后續再專門介紹。
DTO的映射
聚合與DTO的轉換,看上去是一個簡單問題,在聚合與DTO幾乎完全一致的情況下,采用映射組件將非常省力。很多人采用AutoMapper,但它的性能稍微差了點,EmitMapper是更好的選擇,性能接近硬編碼。
當DTO與聚合顯著不同時,我發現手工編碼更加清晰高效。我采用代碼生成器創建出一個代碼基礎,在有個性化需求時,手工修改映射代碼。
我總是采用一個靜態類來擴展DTO和聚合,為它們添加相關的轉換方法。
using Biz.Security.Domains.Models; using Util; namespace Biz.Security.Services.Dtos { /// <summary>
/// 應用程序數據傳輸對象擴展 /// </summary>
public static class ApplicationDtoExtension { /// <summary>
/// 轉換為應用程序實體 /// </summary>
/// <param name="dto">應用程序數據傳輸對象</param>
public static Application ToEntity( this ApplicationDto dto ) { return new Application( dto.Id.ToGuid() ) { Code = dto.Code, Name = dto.Name, Note = dto.Note, Enabled = dto.Enabled, CreateTime = dto.CreateTime, Version = dto.Version, }; } /// <summary>
/// 轉換為應用程序數據傳輸對象 /// </summary>
/// <param name="entity">應用程序實體</param>
public static ApplicationDto ToDto( this Application entity ) { return new ApplicationDto { Id = entity.Id.ToString(), Code = entity.Code, Name = entity.Name, Note = entity.Note, Enabled = entity.Enabled, CreateTime = entity.CreateTime, Version = entity.Version, }; } } }
DTO 與 ViewModel比較
ViewModel是為特定視圖專門定義的實體對象,專為該視圖服務。
對於WPF,ViewModel是必須的,用來支持MVVM模式進行雙向綁定。
那么MVC呢,一定需要它嗎?
由於采用了DTO,在一般情況下,我都把這個DTO當作ViewModel來使用。如果界面上需要某個屬性,我會直接添加到DTO上。
一個例外是,如果MVC的界面非常復雜,我感覺把大量的垃圾屬性加到DTO上不合適,就會創建專門的ViewModel。
查詢實體介紹
查詢實體這個說法,是我亂取的,估計你在其它地方也沒有聽說過。使用它的原因,是用來配合我的查詢組件一起工作。
我前面已經介紹過查詢相關的內容,核心思想是通過判斷一個可空屬性,自動完成空值判斷,這是一個強大的特性,幫助你免於編寫大量雜亂無章的判斷。
查詢實體的基本特征就是所有屬性必須可空,並且它足夠簡單,不會擁有集合那樣的子對象,所有屬性都是扁平化的。
通過傳遞查詢實體,表現層可以做到盡量簡單,由於表現層支持模型綁定,甚至不需要代碼,省力是我搭建框架的一個基本出發點。
當然查詢實體只支持簡單查詢,不支持靈活的動態查詢,比如讓客戶設置查詢運算符等,暫時沒有這方面的需求,如果后續有需求,會擴展一個出來。
查詢實體示例:
using System.ComponentModel.DataAnnotations; using Util; using Util.Domains.Repositories; namespace Biz.Security.Domains.Queries { /// <summary>
/// 應用程序查詢實體 /// </summary>
public class ApplicationQuery : Pager { /// <summary>
/// 應用程序編號 /// </summary>
[Display( Name = "應用程序編號" )] public System.Guid? ApplicationId { get; set; } private string _code = string.Empty; /// <summary>
/// 應用程序編碼 /// </summary>
[Display( Name = "應用程序編碼" )] public string Code { get { return _code == null ? string.Empty : _code.Trim(); } set { _code = value; } } private string _name = string.Empty; /// <summary>
/// 應用程序名稱 /// </summary>
[Display( Name = "應用程序名稱" )] public string Name { get { return _name == null ? string.Empty : _name.Trim(); } set { _name = value; } } private string _note = string.Empty; /// <summary>
/// 備注 /// </summary>
[Display( Name = "備注" )] public string Note { get { return _note == null ? string.Empty : _note.Trim(); } set { _note = value; } } /// <summary>
/// 啟用 /// </summary>
[Display( Name = "啟用" )] public bool? Enabled { get; set; } /// <summary>
/// 起始創建時間 /// </summary>
[Display( Name = "起始創建時間" )] public System.DateTime? BeginCreateTime { get; set; } /// <summary>
/// 結束創建時間 /// </summary>
[Display( Name = "結束創建時間" )] public System.DateTime? EndCreateTime { get; set; } /// <summary>
/// 添加描述 /// </summary>
protected override void AddDescriptions() { base.AddDescriptions(); AddDescription( "應用程序編號", ApplicationId ); AddDescription( "應用程序編碼", Code ); AddDescription( "應用程序名稱", Name ); AddDescription( "備注", Note ); AddDescription( "啟用", Enabled.Description() ); AddDescription( "起始創建時間", BeginCreateTime ); AddDescription( "結束創建時間", EndCreateTime ); } } }
總結
最后來總結一下:
1. 領域實體是系統的中心,是業務邏輯的主要放置場所,應該盡量關閉業務邏輯操作的屬性,以避免有人能繞過你的方法直接操作數據。
2. DTO是數據傳輸對象,原義是用來在分布式系統中一次傳輸更多數據,以減少調用次數,提升性能。
3. 我的DTO用法離原義相去甚遠,只是借用了DTO的名詞,屬於變種。DTO為我解決了如下幾個問題:
- 領域實體在表現層進行模型綁定時可能失敗
- 序列化領域實體可能失敗
- 領域實體無法應對多客戶端應用需求,通過創建多套DTO甚至應用層,可以為不同的應用提供服務,而領域層不變,它是系統的中心。
4. DTO是包含大量屬性,沒有方法的貧血實體,所有屬性都開放getter和setter,以方便模型綁定和序列化。
5. DTO一般情況下是聚合去除方法后的模樣,主要好處是方便抽象CRUD及代碼生成。
6. DTO位於應用層,由應用層服務操作它。
7. DTO的映射可以采用映射組件,也可以代碼生成方便隨時修改,以你覺得方便為主。
8. 僅在WPF環境下才需要為每個視圖創建一個對應的ViewModel,MVC一般使用DTO即可,僅為復雜界面創建ViewModel。
9. 查詢實體是為了配合查詢組件引入的構造,目的是幫助查詢組件完成空值判斷,並且簡化表現層的調用。
本文分享了我在幾個構造類型上的認識和經驗,希望大家積極討論,更希望高手能指正我的不足,幫助我與大家一起進步。
.Net應用程序框架交流QQ群: 386092459,歡迎有興趣的朋友加入討論。
.Net Easyui開發交流QQ群(本群僅限Easyui開發者,非Easyui開發者勿進):157809322
謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/xiadao521/