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

  • 相关阅读:
    比赛-ZR DAY1 (04 Aug, 2018)
    Java NIO系列教程(十一) Pipe
    Java NIO系列教程(九) ServerSocketChannel
    Java NIO系列教程(十) Java NIO DatagramChannel
    Java NIO系列教程(七) FileChannel
    Java NIO系列教程(八) SocketChannel
    Java NIO系列教程(六) Selector
    Java NIO系列教程(四) Scatter/Gather
    Java NIO系列教程(五) 通道之间的数据传输
    Java NIO系列教程(二) Channel
  • 原文地址:https://www.cnblogs.com/daxnet/p/1780764.html
Copyright © 2020-2023  润新知