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模式】

 

还记得在《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的操作约束在领域实体上,同时也提供了强类型支持。

EntityFramework之领域驱动设计实践【规约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中的实现作了简要的讨论,文中也会存在欠考虑的地方,欢迎各位网友各抒己见,提出宝贵意见。

EntityFramework之领域驱动设计实践【规约Specification模式】

上一篇:EntityFramework之领域驱动设计实践【服务Services】


下一篇:EntityFramework之领域驱动设计实践【仓储深入实现】