應用程序框架實戰十五:DDD分層架構之領域實體(驗證篇)

  在應用程序框架實戰十四:DDD分層架構之領域實體(基礎篇)一文中,我介紹了領域實體的基礎,包括標識、相等性比較、輸出實體狀態等。本文將介紹領域實體的一個核心內容——驗證,它是應用程序健壯性的基石。爲了完成領域實體的驗證,咱們在前面已經準備好了驗證公共操做類異常公共操做類html

  .Net提供的DataAnnotations驗證方法很是強大,Mvc會自動將DataAnnotations特性轉換爲客戶端Js驗證,從而提高了用戶體驗。可是客戶端驗證是靠不住的,由於很容易繞開界面向服務端提交數據,因此服務端必須從新驗證。換句話說,服務端驗證纔是必須的,客戶端驗證只是爲了提高用戶體驗而已。程序員

  爲了在服務端可以進行驗證,Mvc提供了ModelState.IsValid。數據庫

[HttpPost] public ActionResult 方法名( 實體名 model ) { if ( ModelState.IsValid == false ) { //驗證失敗就返回,可能會添加錯誤消息,也可能要轉換爲客戶端能識別的消息格式
 } //驗證成功就執行後面的代碼
}

  在控制器裏寫if ( ModelState.IsValid == false )判斷有幾個問題,下面進行一些討論。架構

  第一,可能誤導初學者,致使分層不清。框架

  從分層架構的角度來說,驗證屬於業務層,在DDD分層架構就是領域層。觀察ModelState.IsValid能夠發現,這句代碼並非在定義驗證規則,而是調用驗證。在控制器上直接調用驗證可能並非什麼問題,但初學者可能會認爲,既然能夠在控制器上調用ModelState.IsValid進行驗證,那麼其它驗證代碼也能夠放到控制器上。ide

 [HttpPost] public ActionResult 方法名( 實體名 model ) { if ( ModelState.IsValid == false ) { //驗證失敗就返回
 } if ( model.A > 1 ) { //驗證失敗就返回
 } if ( model.B > 2 ) { //驗證失敗就返回
 } //驗證成功就執行後面的代碼
        }

  觀察上面代碼,model.A > 1 已經將本屬於領域層的驗證定義規則泄露到表現層來了,由於這句代碼訪問了實體的屬性,所謂驗證規則,就是對實體屬性值進行某些約束。測試

  既然能夠在控制器上寫驗證,那麼就會有人在這裏寫業務邏輯,因此到了後面,DDD分層架構如同虛設。ui

  第二,錯誤的驗證時機可能致使驗證失敗。this

  考慮這樣的場景,若是實體中某些屬性須要調用特定方法來產生結果,當提交到控制器操做時,這些屬性仍是空值,因爲尚未調用特定方法,因此調用ModelState.IsValid可能致使驗證失敗。spa

  能夠看出,這實際上是由於驗證的時機不對,驗證幾乎必定要在某些操做以後來進行,好比初始化操做,固然你能夠在調用ModelState.IsValid以前調用特定方法,但這會致使分層不清的問題。

  打個比方,實體中有一個訂單號,它是一個字符串類型,而且添加了[Required]特性,須要調用某個方法來建立訂單號,當訂單實體被提交到控制器操做時,調用ModelState.IsValid就會失敗,由於訂單號如今是空值。固然你能夠把生成訂單號的操做提早到建立訂單界面以前,這樣再提交過來就沒問題了,在這個例子上通常是可行的,但有些操做你可能沒法提早。

  第三,沒法保證驗證完整性,可能須要屢次驗證。

  不少時候,DataAnnotations沒法知足咱們的需求,因此咱們還須要爲特定業務需求寫一些定製的驗證代碼。而ModelState.IsValid只能驗證DataAnnotations特性,因此這時候驗證經過意義不大,由於你須要在後面再驗證一次。固然你能夠經過一些手段進行擴展,讓ModelState.IsValid可以驗證你的特定規則,但沒有多大必要,由於表現層在分層上的要點就是儘可能不要寫代碼。

  第四,致使冗餘代碼。

  如今來觀察每一個ModelState.IsValid判斷都幹了些什麼工做,通常都會轉換成客戶端的特定消息,好比某種格式的Json,而後返回給客戶端顯示出來。爲了這樣一個簡單的功能,須要在大量的方法上添加這個判斷嗎?更好的方法是把這個判斷抽象到控制器基類,由基類來進行處理,其它地方有錯誤拋出異常就能夠了。這樣能夠獲得一個統一的異常處理模型,而且消除了大量冗餘代碼。從這裏也能夠看出,打造你的應用程序框架,老是從這些不起眼的地方着手,反覆考慮每一個判斷,每行代碼是否是能夠消滅,把儘可能多的東西抽象到框架中,這樣在開發過程當中更多工做就會自動完成,不斷提煉可讓你的工做愈來愈輕鬆。

  綜上所述,在表現層進行驗證並非一個好方法,執行驗證能夠在應用層,而定義驗證就必定要在領域層。下面開始介紹如何對領域實體進行驗證支持。

  如今有一個員工實體,叫Employee,以下所示。

    /// <summary>
    /// 員工 /// </summary>
    public class Employee : EntityBase { /// <summary>
        /// 姓名 /// </summary>
        [Required( ErrorMessage = "姓名不能爲空" )] public string Name { get; set; } /// <summary>
        /// 性別 /// </summary>
        [Required( ErrorMessage = "性別不能爲空" )] public string Gender { get; set; } /// <summary>
        /// 年齡 /// </summary>
        [Range(18,50,ErrorMessage = "年齡範圍爲18歲到50歲")] public int Age { get; set; } /// <summary>
        /// 職業 /// </summary>
        [Required(ErrorMessage = "職業不能爲空")] public string Job { get; set; } /// <summary>
        /// 工資 /// </summary>
        public double Salary { get; set; } }

  爲了簡單起見,我把一些東西簡化了,好比性別用枚舉更好,但用了字符串類型,而年齡根據出生年月推斷會更好等等。這個例子只是想說明驗證的方法,因此不用考慮它的真實性。

  能夠看見,在員工實體的屬性上添加了一些DataAnnotations特性,這些特性保證了基本的驗證。如今定義了驗證規則,那麼怎麼執行驗證呢?前面已經說了,用ModelState.IsValid雖然能夠實現這個功能,但不是最優方法,因此咱們要另謀出路。

  執行驗證的最簡單方法可能長成這樣:employee.Validate(),employee是Employee的實例,Validate是Employee中的一個實例方法。

  注意,如今咱們在領域實體中定義了一個方法,這可能會打破你平時的習慣和認識。多年的習慣可能讓你對實體的認識就是,只有一堆屬性的對象。如今要把思惟轉變過來,這個轉變相當重要,它是你進入面向對象開發的第一步。

  想一想看,你如今要進行驗證,應該上哪才能找到這個能執行驗證的方法呢?若是它不在實體中,那麼它可能在表現層,也可能在應用層,還可能在領域服務中,固然還有可能不存在,都還沒人實現呢。

  因此咱們須要給業務邏輯安家,這樣才能幫你統一的管理業務邏輯,並提供惟一的訪問點。這個家最好的地方就是實體自己,由於屬性全都在這裏面,屬性上執行的邏輯也所有放進來,就能實現對象級別的高內聚。當屬性和邏輯發生變化時,對外的方法接口可能不變,這時候全部變化引發的影響就被限制在實體內部,這樣就達到了更低的耦合。

  下面,咱們來實現Validate方法。

  首先考慮,這個方法應該被定義在哪呢?是否是每一個實體上都定義一個,因爲驗證對於絕大部分實體都是必須的功能,因此須要定義到層超類型上,即EntityBase。

  再來考慮一下Validate的方法簽名。須要一個返回值嗎,好比bool值,我在以前的文章已經討論了返回bool值來指示是否驗證經過不是一個好方法,因此咱們如今返回void。那麼方法參數呢?因爲如今是直接在實體上調用,因此參數也不是必須的。

        /// <summary>
        /// 驗證 /// </summary>
        public void Validate() { }

  爲了實現這個方法,咱們必需要可以驗證明體上的DataAnnotations特性,這在前面的驗證公共操做類已經準備好了。咱們在Util.Validations命名空間中定義了IValidation接口,並使用企業庫實現了這個接口。

  考慮在EntityBase的Validate方法中該如何得到IValidation的實例呢?依賴程度最低的方法是使用構造方法注入。

    /// <summary>
    /// 領域實體 /// </summary>
    /// <typeparam name="TKey">標識類型</typeparam>
    public abstract class EntityBase<TKey> { /// <summary>
        /// 驗證器 /// </summary>
        private IValidation _validation; /// <summary>
        /// 標識 /// </summary>
 [Required] public TKey Id { get; private set; } /// <summary>
        /// 初始化領域實體 /// </summary>
        /// <param name="id">標識</param>
        /// <param name="validation">驗證器</param>
        protected EntityBase( TKey id, IValidation validation ) { Id = id; _validation = validation; } }

  在外部經過構造方法把須要的驗證器實例傳進來,這樣甚至不須要在Util.Domains中引用任何程序集。這看起來很誘人,但不要盲目的追求低耦合。考慮驗證器的穩定性,這應該很是高,你基本不會隨便換掉它,更不會動態更換它。再看構造方法,多了一個參數,這會致使實體使用起來很是困難。因此爲了避免必要的擴展性犧牲易用性,並不划算。

  另外一種方法是經過Validate方法的參數注入,這樣可能要好些,但仍是會讓方法在調用時變得難用。

  應用程序框架只是給你或你的團隊在小範圍使用的,它不像.Net Framework或第三方框架在全球範圍使用,因此你沒有必要追求很是高的擴展性,若是發生變化致使你須要修改應用程序框架,你打開來改一下也不是啥大問題,由於框架和項目源碼都在你的控制範圍內,不見得非要達到OCP原則。固然,若是發生變化的可能性高,你仍是須要考慮下降依賴。在依賴性和易用性間取捨,必定要根據實際狀況,不要盲目追求低耦合。

  另外再考慮每一個實體可能須要更換不一樣的驗證器嗎?若是須要,那就得引入工廠方法模式。因爲這個驗證器只是用來驗證DataAnnotations特性的,因此沒這必要。

  那麼直接在EntityBase中new一個Validation實例好很差呢?嘿嘿,這我也只能說要求過低了。一個折中的方案是使用簡單靜態工廠,若是須要更換驗證器實現,你就把這個工廠打開來改改,其它地方不動,通常來說這已經夠用。

  爲Util.Domains引用Util.Validations.EntLib程序集,並在Util.Domains中添加ValidationFactory類。

using Util.Validations; using Util.Validations.EntLib; namespace Util.Domains { /// <summary>
    /// 驗證工廠 /// </summary>
    public class ValidationFactory { /// <summary>
        /// 建立驗證操做 /// </summary>
        public static IValidation Create() { return new Validation(); } } }

  在EntityBase類中添加Validate方法。

        /// <summary>
        /// 驗證 /// </summary>
        public void Validate() { var result = ValidationFactory.Create().Validate( this ); if ( result.IsValid ) return; throw new Warning( result.First().ErrorMessage ); }

  咱們在Validate方法中將領域實體自己傳入Validation實例中進行驗證,得到驗證結果之後,判斷若是驗證失敗就拋出異常,這裏的異常是咱們在上一篇定義的異常公共操做類Warning,這樣咱們就知道是業務上發生了錯誤,能夠把這個拋出的消息顯示給客戶。

  完成了上面的步驟之後,就能夠進行基本的驗證了。可是隻能用DataAnnotations進行基本驗證,很明顯沒法知足咱們的實際需求。

  如今來假想一個驗證需求,你的老闆是個好人,大家的人力資源系統也是本身開發的,他要求程序員老男人的工資不能小於一萬。換句話說,若是是一個程序員老男人,他的信息被保存到數據庫的時候,工資不能小於一萬,不然就是非法數據。程序員老男人這個詞彙很明顯不存在,爲了加深你的印象,用它來給你演示業務概念如何被映射到系統中。

  程序員老男人包含三個條件:

  1. 職業 == 程序員
  2. 年齡 > 40
  3. 性別 == 男

  你爲了驗證這個需求,能使用DataAnnotations特性嗎,也許你真的能夠,可是大部分人都作不到,哪怕作到也異常複雜。

  爲了實現這個功能,你可能在調用了Validate()方法以後,緊接着進行判斷。

 employee.Validate(); if ( employee.Job == "程序員" && employee.Age > 40 && employee.Gender == "" && employee.Salary < 10000 ) throw new Warning( "程序員老男人的工資不能低於1萬" );

  若是你調用Validate是在應用層,這下好了,把驗證邏輯泄露到應用層去了,很快,你的分層架構就會亂成一團。

  時刻記住,只要是業務邏輯,你就必定要放到領域層。驗證是業務邏輯的一個重要組成部分,這就是說,沒有驗證,業務邏輯多是錯的,由於進來的數據不在合法範圍。

  如今把這句判斷移到Employee實體,最合適的地方就是Validate方法中,但這個方法是在基類EntityBase上定義的,爲了可以給基類方法添加行爲,能夠把EntityBase中的Validate方法設爲虛方法,這樣子類就能夠重寫了。

  基類EntityBase中的Validate方法修改以下。

        /// <summary>
        /// 驗證 /// </summary>
        public virtual void Validate() { var result = ValidationFactory.Create().Validate( this ); if ( result.IsValid ) return; throw new Warning( result.First().ErrorMessage ); }

  在Employee實體中重寫Validate方法,注意必須調用base.Validate(),不然對DataAnnotations的驗證將丟失。

        public override void Validate() { base.Validate(); if ( Job == "程序員" && Age > 40 && Gender == "" && Salary < 10000 ) throw new Warning( "程序員老男人的工資不能低於1萬" ); }

  對於應用層來說,它並不關心具體怎麼驗證,它只知道調用employee.Validate()就好了。這樣就把驗證給封裝了起來,爲應用層提供了一個清晰而簡單的API。

  通常說來,DataAnnotations和重寫Validate方法添加自定義驗證能夠知足大部分領域實體的驗證需求。可是,若是驗證規則不少,並且很複雜,會發現重寫的Validate方法很快變成一團亂麻。

  除了代碼雜亂無章以外,還有一個問題是,業務概念被淹沒在大量的條件判斷中,好比Job == "程序員" && Age > 40 && Gender == "男" && Salary < 10000這個條件實際上表明的業務概念是程序員老男人的工資規則。

  另外一個問題是,有些驗證規則只在某些特定條件下進行,直接固化到實體中並不合適。

  當驗證變得逐漸複雜時,就須要考慮將驗證從實體中拆分出來。將一條驗證規則封裝到一個驗證規則對象中,這就是規約模式在驗證上的應用。規約的概念很簡單,它是一個謂詞,用來測試一個對象是否知足某些條件。規約的強大之處在於,將一堆相關的條件表達式封裝起來,清晰的表達了業務概念。

  把程序員老男人的工資規則提取到一個OldProgrammerSalaryRule類中,以下所示。

    /// <summary>
    /// 程序員老男人的工資驗證規則 /// </summary>
    public class OldProgrammerSalaryRule { /// <summary>
        /// 初始化程序員老男人的工資驗證規則 /// </summary>
        /// <param name="employee">員工</param>
        public OldProgrammerSalaryRule( Employee employee ) { _employee = employee; } /// <summary>
        /// 員工 /// </summary>
        private readonly Employee _employee; /// <summary>
        /// 驗證 /// </summary>
        public bool Validate() { if ( _employee.Job == "程序員" && _employee.Age > 40 && _employee.Gender == "" && _employee.Salary < 10000 ) return false; return true; } }

  上面的驗證規則對象,經過構造方法接收業務實體,而後經過Validate方法進行驗證,若是驗證失敗就返回false。

  返回bool值的一個問題是,錯誤描述就拿不到了。爲了得到錯誤描述,我把返回類型從bool改爲ValidationResult。

using System.ComponentModel.DataAnnotations; namespace Util.Domains.Tests.Samples { /// <summary>
    /// 程序員老男人的工資驗證規則 /// </summary>
    public class OldProgrammerSalaryRule { /// <summary>
        /// 初始化程序員老男人的工資驗證規則 /// </summary>
        /// <param name="employee">員工</param>
        public OldProgrammerSalaryRule( Employee employee ) { _employee = employee; } /// <summary>
        /// 員工 /// </summary>
        private readonly Employee _employee; /// <summary>
        /// 驗證 /// </summary>
        public ValidationResult Validate() { if ( _employee.Job == "程序員" && _employee.Age > 40 && _employee.Gender == "" && _employee.Salary < 10000 ) return new ValidationResult( "程序員老男人的工資不能低於1萬" ); return ValidationResult.Success; } } }

  驗證規則對象雖然抽出來了,可是在哪調用它呢?最好的地方就是領域實體的Validate方法,由於這樣應用層將很是簡單。

  爲了可以在領域實體的Validate方法中調用驗證規則對象,須要將驗證規則添加到該實體中,這能夠在Employee中增長一個AddValidationRule方法。

    /// <summary>
    /// 員工 /// </summary>
    public class Employee : EntityBase { //構造方法和屬性

        /// <summary>
        /// 驗證規則集合 /// </summary>
        private List<OldProgrammerSalaryRule> _rules; /// <summary>
        /// 添加驗證規則 /// </summary>
        /// <param name="rule">驗證規則</param>
        public void AddValidationRule( OldProgrammerSalaryRule rule ) { if ( rule == null ) return; _rules.Add( rule ); } /// <summary>
        /// 驗證 /// </summary>
        public override void Validate() { base.Validate(); foreach ( var rule in _rules ) { var result = rule.Validate(); if ( result == ValidationResult.Success ) continue; throw new Warning( result.ErrorMessage ); } } }

  若是另外一個領域實體須要使用驗證規則,就要複製代碼過去改一下,這顯然是不行的,因此須要把添加驗證規則抽到基類EntityBase中。爲了支持這個功能,首先要爲驗證規則抽象出一個接口,代碼以下。

using System.ComponentModel.DataAnnotations; namespace Util.Validations { /// <summary>
    /// 驗證規則 /// </summary>
    public interface IValidationRule { /// <summary>
        /// 驗證 /// </summary>
 ValidationResult Validate(); } }

  在EntityBase中添加AddValidationRule方法,並修改Validate方法,代碼以下。

        /// <summary>
        /// 驗證規則集合 /// </summary>
        private readonly List<IValidationRule> _rules; /// <summary>
        /// 添加驗證規則 /// </summary>
        /// <param name="rule">驗證規則</param>
        public void AddValidationRule( IValidationRule rule ) { if ( rule == null ) return; _rules.Add( rule ); } /// <summary>
        /// 驗證 /// </summary>
        public virtual void Validate() { var result = ValidationFactory.Create().Validate( this ); foreach ( var rule in _rules ) result.Add( rule.Validate() ); if ( result.IsValid ) return; throw new Warning( result.First().ErrorMessage ); }

  如今讓OldProgrammerSalaryRule實現IValidationRule接口,應用層能夠像下面這樣調用。

employee.AddValidationRule( new OldProgrammerSalaryRule( employee ) ); employee.Validate();

  能夠在幾個地方爲領域實體設置驗證規則對象。

  1. 領域實體的構造方法中。
  2. 具體的領域實體重寫Validate方法中。
  3. 當工廠建立領域實體(聚合)時。
  4. 領域服務或應用服務調用領域實體進行驗證時。

  設置驗證規則的要點是,穩定的驗證規則儘可能放到實體中,以方便使用。

  如今還有一個問題是,驗證處理是拋出一個異常,這個異常的消息設置爲驗證結果集合的第一個消息。這在大部分時候都夠用了,可是某些時候對錯誤的處理會有所不一樣,好比你如今要顯示所有驗證失敗的消息,這時候將要修改框架。因此把驗證的處理提取出來是個不錯的方法。

  定義一個驗證處理的接口IValidationHandler,這個驗證處理接口有一個Handle的處理方法,接收一個驗證結果集合的參數,代碼以下。

    /// <summary>
    /// 驗證處理器 /// </summary>
    public interface IValidationHandler { /// <summary>
        /// 處理驗證錯誤 /// </summary>
        /// <param name="results">驗證結果集合</param>
        void Handle( ValidationResultCollection results ); }

  因爲只須要在特殊狀況下更換驗證處理實現,因此定義一個默認的實現,代碼以下。

    /// <summary>
    /// 默認驗證處理器,直接拋出異常 /// </summary>
    public class ValidationHandler : IValidationHandler{ /// <summary>
        /// 處理驗證錯誤 /// </summary>
        /// <param name="results">驗證結果集合</param>
        public void Handle( ValidationResultCollection results ) { if ( results.IsValid ) return; throw new Warning( results.First().ErrorMessage ); } }

  爲了可以更換驗證處理器,須要在EntityBase中提供一個方法SetValidationHandler,代碼以下。

        /// <summary>
        /// 驗證處理器 /// </summary>
        private IValidationHandler _handler; /// <summary>
        /// 設置驗證處理器 /// </summary>
        /// <param name="handler">驗證處理器</param>
        public void SetValidationHandler( IValidationHandler handler ) { if ( handler == null ) return; _handler = handler; }

  在EntityBase構造方法中初始化_handler = new ValidationHandler(),並修改Validate方法。

   

        /// <summary>
        /// 驗證 /// </summary>
        public virtual void Validate() { var result = ValidationFactory.Create().Validate( this ); foreach ( var rule in _rules ) result.Add( rule.Validate() ); if ( result.IsValid ) return; _handler.Handle( result ); }

  最後,用提取方法重構來改善一下Validate代碼。

 

        /// <summary>
        /// 驗證 /// </summary>
        public virtual void Validate() { var result = GetValidationResult(); HandleValidationResult( result ); } /// <summary>
        /// 獲取驗證結果 /// </summary>
        private ValidationResultCollection GetValidationResult() { var result = ValidationFactory.Create().Validate( this ); Validate( result ); foreach ( var rule in _rules ) result.Add( rule.Validate() ); return result; } /// <summary>
        /// 驗證並添加到驗證結果集合 /// </summary>
        /// <param name="results">驗證結果集合</param>
        protected virtual void Validate( ValidationResultCollection results ) { } /// <summary>
        /// 處理驗證結果 /// </summary>
        private void HandleValidationResult( ValidationResultCollection results ) { if ( results.IsValid ) return; _handler.Handle( results ); }

 

  注意,這裏添加了一個Validate( ValidationResultCollection results )虛方法,這是一個鉤子方法,提供它的目的是容許子類向ValidationResultCollection中添加自定義驗證的結果。它和重寫Validate()方法的區別是,若是重寫Validate()方法,那麼你將須要本身處理驗證,而Validate( ValidationResultCollection results )方法將以統一的方式被handler處理。

  這樣,咱們就實現了驗證規則定義與驗證處理的分離。

  最後,再對這個小例子完善一下,能夠將「程序員老男人」這個概念封裝到Employee的一個方法中。

        /// <summary>
        /// 是否程序員老男人 /// </summary>
        public bool IsOldProgrammer() { return Job == "程序員" && Age > 40 && Gender == ""; }

  OldProgrammerSalaryRule驗證規則的實現修改成以下代碼。

        /// <summary>
        /// 驗證 /// </summary>
        public ValidationResult Validate() { if ( _employee.IsOldProgrammer() && _employee.Salary < 10000 ) return new ValidationResult( "程序員老男人的工資不能低於1萬" ); return ValidationResult.Success; }

  這樣不只概念上更清晰,並且當多個地方須要對「程序員老男人」進行驗證時,還能體現出更強的封裝性。

  因爲代碼較多,完整代碼就不粘貼了,若有須要請自行下載。

  若是你有更好的驗證方法,請必定要告訴我,等我理解之後分享給你們。

  .Net應用程序框架交流QQ羣: 386092459,歡迎有興趣的朋友加入討論。

  謝謝你們的持續關注,個人博客地址:http://www.cnblogs.com/xiadao521/

  下載地址:http://files.cnblogs.com/xiadao521/Util.2014.11.20.1.rar

相關文章
相關標籤/搜索