• EntityFramework之领域驱动设计实践(十)(转)


    http://www.cnblogs.com/daxnet/archive/2010/07/19/1780764.html
    规约(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. }

    接下来的一步就是实现这个接口,并在类中分别实现接口中的方法。很明显,在这个接口中,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. }

    还是先看例子吧。在引入规约以后,上面的代码就可以修改为:

    隐藏行号 复制代码 规约的引入
    1. public interface ICustomerRepository
    2.  {
    3.     Customer GetBySpecification(Specification spec);
    4.     IList<Customer> GetAllBySpecification(Specification spec);
    5. }

    6. public class NameSpecification : Specification
    7.  {
    8.     protected string name;
    9.     public NameSpecification(string name) { this.name = name; }
    10.     public override bool IsSatisifedBy(object obj)
    11.     {
    12.         return (obj as Customer).FirstName.Equals(name);
    13.     }
    14. }

    15. public class UserNameSpecification : NameSpecification
    16.  {
    17.     public UserNameSpecification(string name) : base(name) { }
    18.     public override bool IsSatisifedBy(object obj)
    19.     {
    20.         return (obj as Customer).UserName.Equals(this.name);
    21.     }
    22. }

    23. public class RetiredSpecification : Specification
    24.  {
    25.     public override bool IsSatisifedBy(object obj)
    26.     {
    27.         return (obj as Customer).Age >= 60;
    28.     }
    29. }

    30. public class Program1
    31.  {
    32.     static void Main(string[] args)
    33.     {
    34.         ICustomerRepository cr; // = new CustomerRepository();
    35.  Customer getByNameCustomer = cr.GetBySpecification(new NameSpecification("Sunny"));
    36.         Customer getByUserNameCustomer = cr.GetBySpecification(new UserNameSpecification("daxnet"));
    37.         IList<Customer> getRetiredCustomers = cr.GetAllBySpecification(new RetiredSpecification());
    38.     }
    39. }

    通过使用规约,我们将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. }

    针对规约模式实现的讨论,我们才刚刚开始。现在,又出现了下面的问题:

    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. public abstract class Specification : ISpecification
    9.  {

    10.     #region ISpecification Members

    11.     public bool IsSatisfiedBy(object obj)
    12.     {
    13.         return this.Expression.Compile()(obj);
    14.     }

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

    16.     #endregion
    17.  }

    仅仅引入一个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. internal class ExpressionSpec : Specification
      11. {
      12.     private Expression<Func<object, bool>> exp;
      13.     public ExpressionSpec(Expression<Func<object, bool>> expression)
      14.     {
      15.         this.exp = expression;
      16.     }
      17.     public override Expression<Func<object, bool>> Expression
      18.     {
      19.         get { return this.exp; }
      20.     }
      21. }

      22. class Client
      23. {
      24.     static void CallSpec()
      25.     {
      26.         ISpecification spec = Specification.Eval(o => (o as Customer).UserName.Equals("daxnet"));
      27.         // spec....
      28.  }
      29. }

    下图是基于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.     public NHibernateSpecificationParser(ISession session)
    5.     {
    6.         this.session = session;
    7.     }
    8.     #region ISpecificationParser<Expression> Members

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

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

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

    15.  System.Linq.Expressions.Expression expression = query.Expression;
    16.         expression = Evaluator.PartialEval(expression);
    17.         expression = new BinaryBooleanReducer().Visit(expression);
    18.         expression = new AssociationVisitor((ISessionFactoryImplementor)this.session.SessionFactory)
    19.             .Visit(expression);
    20.         expression = new InheritanceVisitor().Visit(expression);
    21.         expression = CollectionAliasVisitor.AssignCollectionAccessAliases(expression);
    22.         expression = new PropertyToMethodVisitor().Visit(expression);
    23.         expression = new BinaryExpressionOrderer().Visit(expression);

    24.         NHibernateQueryTranslator translator = new NHibernateQueryTranslator(this.session);
    25.         var results = translator.Translate(expression, ((INHibernateQueryable)query).QueryOptions);
    26.         ICriteria ca = results as ICriteria;
    27.         
    28.         return ca;
    29.     }

    30.     #endregion
    31. }

    其实,Specification相关的话题远不止本文所讨论的这些,更多内容需要我们在实践中发掘、思考。本文也只是对规约模式及其在.NET中的实现作了简要的讨论,文中也会存在欠考虑的地方,欢迎各位网友各抒己见,提出宝贵意见。

  • 相关阅读:
    js replaceChild
    js hasChildNodes()指针对元素节点子节点多个的话 true
    js:获取节点相关的 nodeName,nodeType,nodeValue
    js 取值 getElementsByTagName,getElementsByName
    小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_5-5.HttpClient4.x工具获取使用
    小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_5-4.微信授权一键登录开发之授权URL获取
    小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_5-3.微信Oauth2.0交互流程讲解
    小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_5-2.微信扫一扫功能开发前期准备
    小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_5-1.数据信息安全--微信授权一键登录功能介绍
    小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_4-3.登录检验JWT实战之封装通用方法
  • 原文地址:https://www.cnblogs.com/quietwalk/p/2230103.html
Copyright © 2020-2023  润新知