程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> EntityFramework之領域驅動設計實踐(十):規約(Specification)模式

EntityFramework之領域驅動設計實踐(十):規約(Specification)模式

編輯:關於.NET

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

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

Customer倉儲的一種設計

public interface ICustomerRespository
  {
   Customer GetByName(string name);
   Customer GetByUserName(string userName);
   IList<Customer> GetAllRetired();
}

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

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

這樣的設計,便於擴展嗎?今後需要添加新的查詢邏輯,結果一大堆相關代碼都要修改,怎麼辦?

隨著時間的推移,這個接口會變得越來越大,團隊中你一榔頭我一棒子地對這個接口進行修改,最後整個設計變得一團糟

GetByName和GetByUserName都OK,因為語義一目了然。但是GetAllRetired呢?什麼是退休?超過法定退休年齡的算退休,那麼病退的是不是算在裡面?這裡返回的所有Customer中,僅僅包含了已退休的男性客戶,還是所有性別的客戶都在裡面?

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

規約

public class Specification
  {
   public virtual bool IsSatisifedBy(object obj)
   {
     return true;
   }
}

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

規約的引入

public interface ICustomerRepository
  {
   Customer GetBySpecification(Specification spec);
   IList<Customer> GetAllBySpecification(Specification spec);
}

public class NameSpecification : Specification
  {
   protected string name;
   public NameSpecification(string name) { this.name = name; }
   public override bool IsSatisifedBy(object obj)
   {
     return (obj as Customer).FirstName.Equals(name);
   }
}

public class UserNameSpecification : NameSpecification
  {
   public UserNameSpecification(string name) : base(name) { }
   public override bool IsSatisifedBy(object obj)
   {
     return (obj as Customer).UserName.Equals(this.name);
   }
}

public class RetiredSpecification : Specification
  {
   public override bool IsSatisifedBy(object obj)
   {
     return (obj as Customer).Age >= 60;
   }
}

public class Program1
  {
   static void Main(string[] args)
   {
     ICustomerRepository cr; // = new CustomerRepository();
     Customer getByNameCustomer = cr.GetBySpecification(new NameSpecification("Sunny"));
     Customer getByUserNameCustomer = cr.GetBySpecification(new UserNameSpecification("daxnet"));
     IList<Customer> getRetiredCustomers = cr.GetAllBySpecification(new RetiredSpecification());
   }
}

通過使用規約,我們將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的倉儲實現

public interface IRepository<TEntity>
   where TEntity : EntityObject, IAggregateRoot
{
   void Add(TEntity entity);
   TEntity GetByKey(int id);
   IEnumerable<TEntity> FindBySpecification(ISpecification spec);
   void Remove(TEntity entity);
   void Update(TEntity entity);
}

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

直接在系統中使用上述規約的實現,效率如何?比如,倉儲對外暴露了一個FindBySpecification的接口。但是,這個接口的實現是怎麼樣的呢?由於規約的IsSatisifedBy方法是基於領域實體的,於是,為了實現根據規約過濾數據,貌似我們只能夠首先從倉儲中獲得所有的對象(也就是數據庫裡所有的記錄),再對這些對象應用給定的規約從而獲得所需要的子集,這樣做肯定是低效的。Evans在其提出Specification模式後,也同樣提出了這樣的問題

從.NET的實踐角度,這樣的設計,能否滿足各種持久化技術的架構設計要求?這個問題與上面第一個問題是如出一轍的。比如,LINQ to Entities采用LINQ查詢對象,而NHibernate又有其自己的Criteria API,Db4o也有自己的LINQ機制。總所周知,Specification是值對象,它是領域層的一部分,同樣也不會去關心持久化技術實現細節。換句話說,我們需要隱藏不同持久化技術架構的具體實現

規約實現的臃腫。根據經典的Specification實現,假設我們需要查找所有過期的、未付款的支票,我們需要創建這樣兩個規約:OverdueSpecification和UnpaidSpecification,然後用Specification的And方法連接兩者,再將完成組合的Specification傳入Repository。時間一長,項目裡充斥著各種Specification,可能其中有相當一部分都只在一個地方使用。雖然將Specification定義為類可以增加模型擴展性,但同時也會使模型變得臃腫。這就有點像.NET裡的委托方法,為了解決類似的問題,.NET引入了匿名方法

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

基於LINQ Expression的規約實現

public interface ISpecification
  {
   bool IsSatisfiedBy(object obj);
   Expression<Func<object, bool>> Expression { get; }

   // Other member goes here...
  }

public abstract class Specification : ISpecification
  {

   #region ISpecification Members

   public bool IsSatisfiedBy(object obj)
   {
     return this.Expression.Compile()(obj);
   }

   public abstract Expression<Func<object, bool>> Expression { get; }

   #endregion
  }

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

通過Expression可以將LINQ查詢直接轉交給持久化機制(如EntityFramework、NHibernate、Db4o等),由持久化機制在從外部數據源獲取數據時執行過濾查詢,從而返回的是經過Specification過濾的結果集,與原本傳統的Specification實現相比,提高了性能

與1同理,基於Expression的Specification是可以通用於大部分持久化機制的

鑒於.NET Framework對LINQ Expression的語言集成支持,我們可以在使用Specification的時候直接編寫Expression,而無需創建更多的類。比如:

Specification Evaluation

public abstract class Specification : ISpecification
  {
   // ISpecification implementation omitted

   public static ISpecification Eval(Expression<Func<object, bool>> expression)
   {
     return new ExpressionSpec(expression);
   }
}

internal class ExpressionSpec : Specification
{
   private Expression<Func<object, bool>> exp;
   public ExpressionSpec(Expression<Func<object, bool>> expression)
   {
     this.exp = expression;
   }
   public override Expression<Func<object, bool>> Expression
   {
     get { return this.exp; }
   }
}

class Client
{
   static void CallSpec()
   {
     ISpecification spec = Specification.Eval(o => (o as Customer).UserName.Equals("daxnet"));
     // spec....
   }
}

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

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

NHibernate Specification Parser

internal sealed class NHibernateSpecificationParser : ISpecificationParser<ICriteria>
{
   ISession session;

   public NHibernateSpecificationParser(ISession session)
   {
     this.session = session;
   }
   #region ISpecificationParser<Expression> Members

   public ICriteria Parse<TEntity>(ISpecification<TEntity> specification)
     where TEntity : class, IEntity
   {
     var query = this.session.Linq<TEntity>().Where(specification.GetExpression());

     //Expression<Func<TEntity, bool>> exp = obj => specification.IsSatisfiedBy(obj);

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

     System.Linq.Expressions.Expression expression = query.Expression;
     expression = Evaluator.PartialEval(expression);
     expression = new BinaryBooleanReducer().Visit(expression);
     expression = new AssociationVisitor((ISessionFactoryImplementor)this.session.SessionFactory)
       .Visit(expression);
     expression = new InheritanceVisitor().Visit(expression);
     expression = CollectionAliasVisitor.AssignCollectionAccessAliases(expression);
     expression = new PropertyToMethodVisitor().Visit(expression);
     expression = new BinaryExpressionOrderer().Visit(expression);

     NHibernateQueryTranslator translator = new NHibernateQueryTranslator(this.session);
     var results = translator.Translate(expression, ((INHibernateQueryable)query).QueryOptions);
     ICriteria ca = results as ICriteria;

     return ca;
   }

   #endregion
}

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved