zoukankan      html  css  js  c++  java
  • 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中的实现作了简要的讨论,文中也会存在欠考虑的地方,欢迎各位网友各抒己见,提出宝贵意见。

  • 相关阅读:
    Elementary Methods in Number Theory Exercise 1.3.13
    Elementary Methods in Number Theory Exercise 1.3.17, 1.3.18, 1.3.19, 1.3.20, 1.3.21
    数论概论(Joseph H.Silverman) 习题 5.3,Elementary methods in number theory exercise 1.3.23
    Elementary Methods in Number Theory Exercise 1.2.31
    数论概论(Joseph H.Silverman) 习题 5.3,Elementary methods in number theory exercise 1.3.23
    Elementary Methods in Number Theory Exercise 1.3.13
    Elementary Methods in Number Theory Exercise 1.3.17, 1.3.18, 1.3.19, 1.3.20, 1.3.21
    Elementary Methods in Number Theory Exercise 1.2.31
    Elementary Methods in Number Theory Exercise 1.2.26 The Heisenberg group
    4__面向对象的PHP之作用域
  • 原文地址:https://www.cnblogs.com/daxnet/p/1780764.html
Copyright © 2011-2022 走看看