规约(Specification)模式
本来针对规约模式的讨论,我并没有想将其列入本系列文章,因为这是一种概念性的东西,从理论上讲,与EntityFramework好像扯不上关系。但应广大网友的要求,我决定还是在这里讨论一下规约模式,并介绍一种专门针对.NET Framework的规约模式实现。
很多时候,我们都会看到类似下面的设计:
-
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的设计,于是,仓储接口可以改为:
-
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的设计做点点修改。代码如下:
-
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。相关代码如下:
-
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中的实现作了简要的讨论,文中也会存在欠考虑的地方,欢迎各位网友各抒己见,提出宝贵意见。