應用程序框架實戰三十四:數據傳輸對象(DTO)介紹及各類型實體比較


  本文將介紹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/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM