您的位置:首页 > 其它

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

2010-07-19 16:12 483 查看
规约(Specification)模式本来针对规约模式的讨论,我并没有想将其列入本系列文章,因为这是一种概念性的东西,从理论上讲,与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中的实现作了简要的讨论,文中也会存在欠考虑的地方,欢迎各位网友各抒己见,提出宝贵意见。// 
                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: