本來針對規約模式的討論,我並沒有想將其列入本系列文章,因爲這是一種概念性的東西,從理論上講,與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中的實現作了簡要的討論,文中也會存在欠考慮的地方,歡迎各位網友各抒己見,提出寶貴意見。
出自:http://www.cnblogs.com/daxnet/archive/2010/07/19/1780764.html