EntityFramework之領域驅動設計實踐(十)

規約(Specification)模式

本來針對規約模式的討論,我並沒有想將其列入本系列文章,因爲這是一種概念性的東西,從理論上講,與EntityFramework好像扯不上關係。但應廣大網友的要求,我決定還是在這裏討論一下規約模式,並介紹一種專門針對.NET Framework的規約模式實現。

很多時候,我們都會看到類似下面的設計:

隱藏行號 複製代碼 Customer倉儲的一種設計
  1. public interface ICustomerRespository
  2.  {
  3.     Customer GetByName(string name);
  4.     Customer GetByUserName(string userName);
  5.     IList<Customer> GetAllRetired();
  6. }
  7. 
    

接下來的一步就是實現這個接口,並在類中分別實現接口中的方法。很明顯,在這個接口中,Customer倉儲一共做了三個操作:通過姓名獲取客戶信息;通過用戶名獲取客戶信息以及獲得所有當前已退休客戶的信息。這樣的設計有一個好處就是一目瞭然,能夠很方便地看到Customer倉儲到底提供了哪些功能。文檔化的開發方式特別喜歡這樣的設計。

還是那句話,應需而變。如果你的系統很簡單,並且今後擴展的可能性不大,那麼這樣的設計是簡潔高效的。但如果你正在設計一箇中大型系統,那麼,下面的問題就會讓你感到困惑:

  1. 這樣的設計,便於擴展嗎?今後需要添加新的查詢邏輯,結果一大堆相關代碼都要修改,怎麼辦?
  2. 隨着時間的推移,這個接口會變得越來越大,團隊中你一榔頭我一棒子地對這個接口進行修改,最後整個設計變得一團糟
  3. GetByName和GetByUserName都OK,因爲語義一目瞭然。但是GetAllRetired呢?什麼是退休?超過法定退休年齡的算退休,那麼病退的是不是算在裏面?這裏返回的所有Customer中,僅僅包含了已退休的男性客戶,還是所有性別的客戶都在裏面?

規約模式就是DDD引入用來解決以上問題的一種特殊的模式。規約是一種布爾斷言,它表述了給定的對象是否滿足當前約定的語義。經典的規約模式實現中,規約類只有一個方法,就是IsSatisifedBy(object);如下:

隱藏行號 複製代碼 規約
  1. public class Specification
  2.  {
  3.     public virtual bool IsSatisifedBy(object obj)
  4.     {
  5.         return true;
  6.     }
  7. }
  8. 
    

還是先看例子吧。在引入規約以後,上面的代碼就可以修改爲:

隱藏行號 複製代碼 規約的引入
  1. public interface ICustomerRepository
  2.  {
  3.     Customer GetBySpecification(Specification spec);
  4.     IList<Customer> GetAllBySpecification(Specification spec);
  5. }
  6. 
    
  7. public class NameSpecification : Specification
  8.  {
  9.     protected string name;
  10.     public NameSpecification(string name) { this.name = name; }
  11.     public override bool IsSatisifedBy(object obj)
  12.     {
  13.         return (obj as Customer).FirstName.Equals(name);
  14.     }
  15. }
  16. 
    
  17. public class UserNameSpecification : NameSpecification
  18.  {
  19.     public UserNameSpecification(string name) : base(name) { }
  20.     public override bool IsSatisifedBy(object obj)
  21.     {
  22.         return (obj as Customer).UserName.Equals(this.name);
  23.     }
  24. }
  25. 
    
  26. public class RetiredSpecification : Specification
  27.  {
  28.     public override bool IsSatisifedBy(object obj)
  29.     {
  30.         return (obj as Customer).Age >= 60;
  31.     }
  32. }
  33. 
    
  34. public class Program1
  35.  {
  36.     static void Main(string[] args)
  37.     {
  38.         ICustomerRepository cr; // = new CustomerRepository();
  39.         Customer getByNameCustomer = cr.GetBySpecification(new NameSpecification("Sunny"));
  40.         Customer getByUserNameCustomer = cr.GetBySpecification(new UserNameSpecification("daxnet"));
  41.         IList<Customer> getRetiredCustomers = cr.GetAllBySpecification(new RetiredSpecification());
  42.     }
  43. }
  44. 
    

通過使用規約,我們將Customer倉儲中所有“特定用途的操作”全部去掉了,取而代之的是兩個非常簡潔的方法:分別通過規約來獲得Customer實體和實體集合。規約模式解耦了倉儲操作與斷言條件,今後我們需要通過倉儲實現其它特定條件的查詢時,只需要定製我們的Specification,並將其注入倉儲即可,倉儲的實現無需任何修改。與此同時,規約的引入,使得我們很清晰地瞭解到,某一次查詢過濾,或者某一次數據校驗是以什麼樣的規則實現的,這給斷言條件的設計與實現帶來了可測試性。

爲了實現複合斷言,通常在設計中引入複合規約對象。這樣做的好處是,可以充分利用規約的複合來實現複雜的規約組合以及規約樹的遍歷。不僅如此,在.NET 3.5引入Expression Tree以後,規約將有其特定的實現方式,這個我們在後面討論。以下是一個經典的實現方式,注意ICompositeSpecification接口,它包含兩個屬性:Left和Right,ICompositeSpecification是繼承於ISpecification接口的,而Left和Right本身也是ISpecification類型,於是,整個Specification的結構就可以看成是一種樹狀結構。


 

還記得在《EntityFramework之領域驅動設計實踐(八)- 倉儲的實現:基本篇》裏提到的倉儲接口設計嗎?當初還沒有牽涉到任何Specification的概念,所以,倉儲的FindBySpecification方法採用.NET的Func<TEntity, bool>委託作爲Specification的聲明。現在我們引入了Specification的設計,於是,倉儲接口可以改爲:

隱藏行號 複製代碼 引入Specification的倉儲實現
  1. public interface IRepository<TEntity>
  2.     where TEntity : EntityObject, IAggregateRoot
  3. {
  4.     void Add(TEntity entity);
  5.     TEntity GetByKey(int id);
  6.     IEnumerable<TEntity> FindBySpecification(ISpecification spec);
  7.     void Remove(TEntity entity);
  8.     void Update(TEntity entity);
  9. }
  10. 
    

針對規約模式實現的討論,我們纔剛剛開始。現在,又出現了下面的問題:

  1. 直接在系統中使用上述規約的實現,效率如何?比如,倉儲對外暴露了一個FindBySpecification的接口。但是,這個接口的實現是怎麼樣的呢?由於規約的IsSatisifedBy方法是基於領域實體的,於是,爲了實現根據規約過濾數據,貌似我們只能夠首先從倉儲中獲得所有的對象(也就是數據庫裏所有的記錄),再對這些對象應用給定的規約從而獲得所需要的子集,這樣做肯定是低效的。Evans在其提出Specification模式後,也同樣提出了這樣的問題
  2. 從.NET的實踐角度,這樣的設計,能否滿足各種持久化技術的架構設計要求?這個問題與上面第一個問題是如出一轍的。比如,LINQ to Entities採用LINQ查詢對象,而NHibernate又有其自己的Criteria API,Db4o也有自己的LINQ機制。總所周知,Specification是值對象,它是領域層的一部分,同樣也不會去關心持久化技術實現細節。換句話說,我們需要隱藏不同持久化技術架構的具體實現
  3. 規約實現的臃腫。根據經典的Specification實現,假設我們需要查找所有過期的、未付款的支票,我們需要創建這樣兩個規約:OverdueSpecification和UnpaidSpecification,然後用Specification的And方法連接兩者,再將完成組合的Specification傳入Repository。時間一長,項目裏充斥着各種Specification,可能其中有相當一部分都只在一個地方使用。雖然將Specification定義爲類可以增加模型擴展性,但同時也會使模型變得臃腫。這就有點像.NET裏的委託方法,爲了解決類似的問題,.NET引入了匿名方法

基於.NET的Specification可以使用LINQ Expression(下面簡稱Expression)來解決上面所有的問題。爲了引入Expression,我們需要對ISpecification的設計做點點修改。代碼如下:

隱藏行號 複製代碼 基於LINQ Expression的規約實現
  1. public interface ISpecification
  2.  {
  3.     bool IsSatisfiedBy(object obj);
  4.     Expression<Func<object, bool>> Expression { get; }
  5.     
  6.     // Other member goes here...
  7.  }
  8. 
    
  9. public abstract class Specification : ISpecification
  10.  {
  11. 
    
  12.     #region ISpecification Members
  13. 
    
  14.     public bool IsSatisfiedBy(object obj)
  15.     {
  16.         return this.Expression.Compile()(obj);
  17.     }
  18. 
    
  19.     public abstract Expression<Func<object, bool>> Expression { get; }
  20. 
    
  21.     #endregion
  22.  }
  23. 
    

僅僅引入一個Expression<Func<object, bool>>屬性,就解決了上面的問題。在實際應用中,我們實現Specification類的時候,由原來的“實現IsSatisfiedBy方法”轉變爲“實現Expression<Func<object, bool>>屬性”。現在主流的.NET對象持久化機制(比如EntityFramework,NHibernate,Db4o等等)都支持LINQ接口,於是:

  1. 通過Expression可以將LINQ查詢直接轉交給持久化機制(如EntityFramework、NHibernate、Db4o等),由持久化機制在從外部數據源獲取數據時執行過濾查詢,從而返回的是經過Specification過濾的結果集,與原本傳統的Specification實現相比,提高了性能
  2. 與1同理,基於Expression的Specification是可以通用於大部分持久化機制的
  3. 鑑於.NET Framework對LINQ Expression的語言集成支持,我們可以在使用Specification的時候直接編寫Expression,而無需創建更多的類。比如:
    隱藏行號 複製代碼 Specification Evaluation
    1. public abstract class Specification : ISpecification
    2.  {
    3.     // ISpecification implementation omitted
    4.  
    5.     public static ISpecification Eval(Expression<Func<object, bool>> expression)
    6.     {
    7.         return new ExpressionSpec(expression);
    8.     }
    9. }
    10. 
      
    11. internal class ExpressionSpec : Specification
    12. {
    13.     private Expression<Func<object, bool>> exp;
    14.     public ExpressionSpec(Expression<Func<object, bool>> expression)
    15.     {
    16.         this.exp = expression;
    17.     }
    18.     public override Expression<Func<object, bool>> Expression
    19.     {
    20.         get { return this.exp; }
    21.     }
    22. }
    23. 
      
    24. class Client
    25. {
    26.     static void CallSpec()
    27.     {
    28.         ISpecification spec = Specification.Eval(o => (o as Customer).UserName.Equals("daxnet"));
    29.         // spec....
    30.     }
    31. }
    32. 
      

     

下圖是基於LINQ Expression的Specification設計的完整類圖。與經典Specification模式的實現相比,除了LINQ Expression的引入外,本設計中採用了IEntity泛型約束,用於將Specification的操作約束在領域實體上,同時也提供了強類型支持。

Specification實現

【如果單擊上圖無法查看圖片,請點擊此處以便查看大圖】

上圖的右上角有個ISpecificationParser的接口,它主要用於將Specification解析爲某一持久化框架可以認識的對象,比如LINQ Expression或者NHibernate的Criteria。當然,在引入LINQ Expression的Specification中,這個接口是可以不去實現的;而對於NHibernate,我們可以藉助NHibernate.Linq命名空間來實現這個接口,從而將Specification轉換爲NHibernate Criteria。相關代碼如下:

隱藏行號 複製代碼 NHibernate Specification Parser
  1. internal sealed class NHibernateSpecificationParser : ISpecificationParser<ICriteria>
  2. {
  3.     ISession session;
  4. 
    
  5.     public NHibernateSpecificationParser(ISession session)
  6.     {
  7.         this.session = session;
  8.     }
  9.     #region ISpecificationParser<Expression> Members
  10. 
    
  11.     public ICriteria Parse<TEntity>(ISpecification<TEntity> specification)
  12.         where TEntity : class, IEntity
  13.     {
  14.         var query = this.session.Linq<TEntity>().Where(specification.GetExpression());
  15. 
    
  16.         //Expression<Func<TEntity, bool>> exp = obj => specification.IsSatisfiedBy(obj);

  17.         //var query = this.session.Linq<TEntity>().Where(exp);

  18.         System.Linq.Expressions.Expression expression = query.Expression;
  19.         expression = Evaluator.PartialEval(expression);
  20.         expression = new BinaryBooleanReducer().Visit(expression);
  21.         expression = new AssociationVisitor((ISessionFactoryImplementor)this.session.SessionFactory)
  22.             .Visit(expression);
  23.         expression = new InheritanceVisitor().Visit(expression);
  24.         expression = CollectionAliasVisitor.AssignCollectionAccessAliases(expression);
  25.         expression = new PropertyToMethodVisitor().Visit(expression);
  26.         expression = new BinaryExpressionOrderer().Visit(expression);
  27. 
    
  28.         NHibernateQueryTranslator translator = new NHibernateQueryTranslator(this.session);
  29.         var results = translator.Translate(expression, ((INHibernateQueryable)query).QueryOptions);
  30.         ICriteria ca = results as ICriteria;
  31.         
  32.         return ca;
  33.     }
  34. 
    
  35.     #endregion
  36. }
  37. 
    

 

其實,Specification相關的話題遠不止本文所討論的這些,更多內容需要我們在實踐中發掘、思考。本文也只是對規約模式及其在.NET中的實現作了簡要的討論,文中也會存在欠考慮的地方,歡迎各位網友各抒己見,提出寶貴意見。

出自:http://www.cnblogs.com/daxnet/archive/2010/07/19/1780764.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章