在應用程序框架實戰十四: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進行基本驗證,很明顯沒法知足咱們的實際需求。
如今來假想一個驗證需求,你的老闆是個好人,大家的人力資源系統也是本身開發的,他要求程序員老男人的工資不能小於一萬。換句話說,若是是一個程序員老男人,他的信息被保存到數據庫的時候,工資不能小於一萬,不然就是非法數據。程序員老男人這個詞彙很明顯不存在,爲了加深你的印象,用它來給你演示業務概念如何被映射到系統中。
程序員老男人包含三個條件:
你爲了驗證這個需求,能使用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();
能夠在幾個地方爲領域實體設置驗證規則對象。
設置驗證規則的要點是,穩定的驗證規則儘可能放到實體中,以方便使用。
如今還有一個問題是,驗證處理是拋出一個異常,這個異常的消息設置爲驗證結果集合的第一個消息。這在大部分時候都夠用了,可是某些時候對錯誤的處理會有所不一樣,好比你如今要顯示所有驗證失敗的消息,這時候將要修改框架。因此把驗證的處理提取出來是個不錯的方法。
定義一個驗證處理的接口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