[.NET领域驱动设计实战系列]专题三:前期准备之规约模式(Specification Pattern)
一、前言
在专题二中已经应用DDD和SOA的思想简单构建了一个网上书店的网站,接下来的专题中将会对该网站补充更多的DDD的内容。本专题作为一个准备专题,因为在后面一个专题中将会网上书店中的仓储实现引入规约模式。本专题将详细介绍了规约模式。
二、什么是规约模式
讲到规约模式,自然想到的是什么是规约模式呢?从名字上看,规约模式就是一个约束条件,我们在使用仓储进行查询的时候,这时候就会牵涉到很多查询条件,例如名字包含C#的书名等条件。这样就自然需要引入规约模式了。规约模式的作用可以自由组装业务逻辑元素。Specification类有一个IsSatisifiedBy函数,用于校验某个对象是否满足该Specification所表达的条件。多个Specification对象可以组装起来,生成新的Specification对象,这样可以通过组装的方式来定制新的条件。简单地说,规约模式就是对查询条件表达式用类的形式进行封装。那这样的话,规约模式引入有什么作用呢?
三、为什么需要引入规约模式模式
上面只是简单介绍了规约模式的作用——可以自由组装业务逻辑元素。这样文字表述未免枯燥了点,下面通过一个具体例子来说明下。
对于在仓储中,我们经常会定义下面的接口
public interface IProductRespository { Product GetById(Guid id); Product GetByName(string name); IEnumerable<Product> GetNewProducts(); }
接下来就是实现这个接口,并在类中分别实现接口中的方法。这样设计的好处就是一目了然,可以方便地看到Product仓储到底提供了哪些功能。
对于这种设计,对于简单系统并且今后扩展的可能性不大,那么这样的设计非常合适,因为其简洁高效。但如果你正在设计一个中大型系统,那么,针对上面的设计,你就需要考虑下面的问题了:
- 今后如果需要添加新的查询逻辑,结果一大堆相关代码都需要修改,上面的设计能便于扩展吗?
- 由于业务的扩展,上面的设计会导致接口变得越来越大,团队成员可能会对这个接口进行修改,添加新的接口方法。
规约模式就是DDD引入解决上面问题的一种模式。下面让我们来看看规约模式的定义与实现。
四、规约模式的传统实现
首先来看下规约模式的类结构图:
上图是摘自维基百科里面的,通过设计图我们很容易实现规约模式,这样之所以称为的传统实现,因为后面会对该实现应用C#的特性来对该实现进行简化,使其更加简单轻量。首先我们需要定义一个ISpecification接口,在接口中定义四个方法:And、Not、Or和IsSatifiedBy方法,具体接口的定义如下所示:
// 规约接口的定义 public interface ISpecification<T> { bool IsSatisfiedBy(T candidate); ISpecification<T> And(ISpecification<T> specification); ISpecification<T> Or(ISpecification<T> specification); ISpecification<T> Not(ISpecification<T> specification); }
实现了ISpeification的对象意味着是一个Specification,即一种筛选条件,我们可以与其他Specification对象通过And、Or和Not操作来生成新的逻辑,即组合成新的筛选条件,为了方便“组合逻辑”的实现,这里还需要定义一个抽象的CompositeSpecification类:
// 因为And,OR和Not方法在所有的Specification都需要实现,只有IsSatisfiedBy方法才依赖业务规则 // 所以为了复用,定义一个抽象类来实现And,Or和And操作,并且留IsSatisfiedBy方法给子类去实现,所以定义其为abstract public abstract class CompositeSpecification<T>: ISpecification<T> { public abstract bool IsSatisfiedBy(T candidate); public ISpecification<T> And(ISpecification<T> specification) { return new AndSpecification<T>(this, specification); } public ISpecification<T> Or(ISpecification<T> specification) { return new OrSpecification<T>(this, specification); } public ISpecification<T> Not(ISpecification<T> specification) { return new NotSpecification<T>(specification); } }
CompositeSpecification提供了构建符合Specification的基础逻辑,它提供了And、Or和Not方法的实现,让其他Specification类只需要专注于IsSatisfiedBy方法的实现即可(这里有点模板方法模式的影子)。下面是And、Or和Not规约的具体实现:
// AndSpecification,OrSpecification and NotSpecification主要为了组合 public class AndSpecification<T> : CompositeSpecification<T> { private readonly ISpecification<T> _lefSpecification; private readonly ISpecification<T> _rightSpecification; public AndSpecification(ISpecification<T> left, ISpecification<T> right) { this._lefSpecification = left; this._rightSpecification = right; } public override bool IsSatisfiedBy(T candidate) { return this._lefSpecification.IsSatisfiedBy(candidate) && this._rightSpecification.IsSatisfiedBy(candidate); } } public class OrSpecification<T> : CompositeSpecification<T> { private readonly ISpecification<T> _leftSpecification; private readonly ISpecification<T> _rightSpecification; public OrSpecification(ISpecification<T> left, ISpecification<T> right) { this._leftSpecification = left; this._rightSpecification = right; } public override bool IsSatisfiedBy(T candidate) { return _leftSpecification.IsSatisfiedBy(candidate) || _rightSpecification.IsSatisfiedBy(candidate); } } public class NotSpecification<T> : CompositeSpecification<T> { private readonly ISpecification<T> _specification; public NotSpecification(ISpecification<T> specification) { this._specification = specification; } public override bool IsSatisfiedBy(T candidate) { return !_specification.IsSatisfiedBy(candidate); } }
接下来我们可以定义具体的规约模式,如果IdEqualSpecification、NameEqualSpecification规约等。下面就看下引入规约模式后,是如何解决上面仓储接口设计所存在的问题的。
// 引入规约模式,IProductRespository接口的定义 public interface IProductRespository { Product GetBySpecification(ISpecification<Product> spec); IEnumerable<Product> FindBySpecification(ISpecification<Product> spec); } public class IdEqualSpecification : CompositeSpecification<Product> { private readonly Guid _id; public IdEqualSpecification(Guid id) { _id = id; } public override bool IsSatisfiedBy(Product candidate) { return candidate.Id.Equals(_id); } } public class NameEqualSpecification : CompositeSpecification<Product> { private readonly string _name; public NameEqualSpecification(string name) { _name = name; } public override bool IsSatisfiedBy(Product candidate) { return candidate.Name.Equals(_name); } } public class NewProductsSpecification : CompositeSpecification<Product> { public override bool IsSatisfiedBy(Product candidate) { return candidate.IsNew == true; } }
通过引入规约后,Product仓储中所有特定用途的操作都删除了,取而代之的是2个非常简洁的方法。规约模式解耦了仓储操作和筛选条件,如果业务扩展,我们可以定制我们的Specification,并将其注入到仓储即可。仓储的接口和实现无需任何修改。
下面通过一个具体的演示例子来看下传统规约模式的应用。具体的场景是这样的:我们想筛选一批int数组中的偶数和大于0的数字出来。因为这里涉及2个筛选条件,一个是偶数,一个是大于0的数,这样我们就可以通过定义偶数规约和正数规约。具体的实现如下所示:
// 具体规约,偶数规约 public class EvenSpecification : CompositeSpecification<int> { public override bool IsSatisfiedBy(int candidate) { return candidate % 2 == 0; } } // 具体的规约,正数规约 public class PlusSpecification : CompositeSpecification<int> { public override bool IsSatisfiedBy(int candidate) { return candidate > 0; } }
接下来通过And操作和将2中规约组合起来形成新的规约。具体的测试代码如下所示:
using spec1 =SpecificationPatternDemo.Specification; class Program { static void Main(string[] args) { Demo1(); Console.Read(); } public static void Demo1() { var items = Enumerable.Range(-5, 10);
Console.WriteLine("产生的数组为:{0}", string.Join(", ", items.ToArray())); spec1.ISpecification<int> evenSpec = new spec1.EvenSpecification(); // 获得一个组合规约 var compositeSpecification = GetCompositeSpecification(evenSpec); // 类似Where(it=>it%2==0 && it > 0) // 前者是把两个条件合并写死成一个条件,而后者是将其组合成一个新条件。就如拼图出一架飞机和直接制造一个飞机模型概念是完全不同的 foreach (var item in items.Where(it=>compositeSpecification.IsSatisfiedBy(it))) { // 输出既是正数又是偶数的数 Console.WriteLine(item); } } private static spec1.ISpecification<int> GetCompositeSpecification(spec1.ISpecification<int> spec) { spec1.ISpecification<int> plusSpec = new spec1.PlusSpecification(); return spec.And(plusSpec); } }
具体的运行结果如下图所示:
上面我们已经介绍完规约模式的实现,并且通过对比的方式来介绍引入规约模式所解决之前的问题。但是传统规约模式的实现显得非常臃肿。因为你想实现一个新的规约,你需要新增一个新的Specification类,这样下来,我们的项目中必然会堆积大量的Specification类。有些规约可能只只使用了一次。这就好比.NET里的委托方法一样,为了解决类似的问题,.NET引入了匿名方法和lamada表达式。同样,我们借助C#的特性也可以使得传统规约模式的实现更轻量。下面就具体看下规约模式的轻量实现是如何去实现的。
五、规约模式的轻量实现
从上面可以看出,规约模式的关键在于IsSatisifiedBy函数,该函数用于校验某个对象是否满足该规约所表示的条件,IsSatisifiedBy函数的返回类型为bool类型,这样我们完全可以让一个ISpecification只具有IsSatisifiedBy函数。然后该函数返回一个委托调用结果。至于原本ISpecification中的And、Or和Not方法,我们将它们提起成扩展方法。经过上面的分析,轻量化后的规约模式实现也就出来了。具体实现如下所示:
public interface ISpecification<in T> { bool IsSatisfiedBy(T candidate); } public class Specification<T> : ISpecification<T> { private readonly Func<T, bool> _isSatisfiedBy; public Specification(Func<T, bool> isSatisfiedBy) { this._isSatisfiedBy = isSatisfiedBy; } public bool IsSatisfiedBy(T candidate) { return _isSatisfiedBy(candidate); } } public static class SpecificationExtensions { public static ISpecification<T> And<T>(this ISpecification<T> left, ISpecification<T> right) { return new Specification<T>(candidate => left.IsSatisfiedBy(candidate) && right.IsSatisfiedBy(candidate)); } public static ISpecification<T> Or<T>(this ISpecification<T> left, ISpecification<T> right) { return new Specification<T>(candidate => left.IsSatisfiedBy(candidate) || right.IsSatisfiedBy(candidate)); } public static ISpecification<T> Not<T>(this ISpecification<T> one) { return new Specification<T>(candidate => !one.IsSatisfiedBy(candidate)); } }
使用扩展方法的好处在于,如果我们要加一个逻辑运行,如异或,那么就不需要修改接口了。修改接口是一个不推荐的的事情。因为接口修改会破坏之前已经发布的接口实现。因此,一旦接口发布之后,它就不能被修改了。这意味着,我们在定义接口时应该仔细推敲,做到接口的职责应该尤其单一。
轻量的实现使得使用Specification对象容易多了,我们不需要为每段逻辑创建一个独立的Specification类。下面具体看下规约模式的轻量实现的使用示例:
using SpecificationPatternDemo.Specification_2; using spec2 = SpecificationPatternDemo.Specification_2; class Program { static void Main(string[] args) { Demo2(); Console.Read(); } public static void Demo2() { var items = Enumerable.Range(-5, 10); Console.WriteLine("产生的数组为:{0}", string.Join(", ", items.ToArray())); spec2.ISpecification<int> evenSpec = new spec2.Specification<int>(it => it % 2 == 0); var compositeSpec = GetCompositeSpecification2(evenSpec); foreach (var i in items.Where(it => compositeSpec.IsSatisfiedBy(it))) { Console.WriteLine(i); } } private static spec2.ISpecification<int> GetCompositeSpecification2(spec2.ISpecification<int> spec) { spec2.ISpecification<int> plusSpec = new spec2.Specification<int>(it => it > 0); return spec.And(plusSpec); } }
从上面的例子可以看出,此时并不需要定义单独的Specification对象了,只需要用委托来代替即可。其运行结果与上面传统实现一样。其实,还可以更简单,我们可以直接使用一个委托,而不不需要定义ISpecification接口和其Specification实现。其实现方式如下所示:
// 更轻量的实现 public static class SpecExtensitions { public static Func<T, bool> And<T>(this Func<T, bool> left, Func<T, bool> right) { return candidate => left(candidate) && right(candidate); } public static Func<T, bool> Or<T>(this Func<T, bool> left, Func<T, bool> right) { return candidate => left(candidate) || right(candidate); } public static Func<T, bool> Not<T>(this Func<T, bool> one) { return candidate => !one(candidate); } }
上面的实现,我们就只需要一个扩展方式就可以了,其使用示例代码如下所示:
class Program { static void Main(string[] args) { Demo3(); Console.Read(); } public static void Demo3() { var items = Enumerable.Range(-5, 10); Console.WriteLine("产生的数组为:{0}", string.Join(", ", items.ToArray())); Func<int, bool> evenSpec = it => it % 2 == 0; var compositeSpec = GetCompositeSpec(evenSpec); foreach (var i in items.Where(it => compositeSpec(it))) { Console.WriteLine(i); } } private static Func<int, bool> GetCompositeSpec(Func<int, bool> spec) { return spec.And(it => it > 0); } }
六、规约模式的轻量实现的完善——对Linq查询支持
上面轻量级的Specification模式抛弃了具体的Specification类型,而是使用一个委托对象关键的IsSatisfiedBy方法。其优势在于使用简单,但是该实现不能支持Linq查询或表达式的场景。因为EF中的DbContext.Dbset集合的进行where筛选参数只能是表达式树。所以我们不能用委托对象来判断逻辑,取而代之的使用表达式树。对于表达式树的构造主要由参数和主体构造,所以针对于Not方法可以如下的方式来实现:
public static class SpecExprExtensions { public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> one) { var candidateExpr = one.Parameters[0]; var body = Expression.Not(one.Body); return Expression.Lambda<Func<T, bool>>(body, candidateExpr); } }
对于Not方法,我们只要获取它的参数表达式,再将它的Body外包一个Not表达式,便可以此构造一个新的表达式了。但And和Or方法实现不能像Not一样简单处理:
// 不能这么处理
public static Expression<Func<T, bool>> And<T>( this Expression<Func<T, bool>> one, Expression<Func<T, bool>> another) { var candidateExpr = one.Parameters[0]; var body = Expression.And(one.Body, another.Body); return Expression.Lambda<Func<T, bool>>(body, candidateExpr); }
因为one和another两个表达式虽然都是同样的形式(Expression<Func<T, bool>>),但是它们的“参数”不是同一个对象。即one.Body和another.Body并没有公用一个ParameterExpression实例,于是无论采用哪个表达式的参数,在Expression.Lambda方法调用的时候,都会出现body中的某个参数对象并没有出现在参数列表中的错误。
既然参数不一致,所以要实现And和Or方法,必须统一两个表达式树的参数。为了达到这个目标,我们可以利用ExpressionVisitor类来实现。这里定义一个派生于ExpressionVisitor的类。具体实现如下:
internal class ParameterReplacer : ExpressionVisitor { public ParameterReplacer(ParameterExpression paramExpr) { this.ParameterExpression = paramExpr; } public ParameterExpression ParameterExpression { get; private set; } public Expression Replace(Expression expr) { return this.Visit(expr); } protected override Expression VisitParameter(ParameterExpression p) { return this.ParameterExpression; } }
Expressionvistor可以用于求值、变形等各种操作。它提供了遍历表达式树的标准方式,如果你直接继承这个类并调用Visit方法(如上面Replace方法的实现一样),那么最终返回的结果便是传入的Expresssion参数本身。但是,如果你覆盖任意一个方法,返回了与传入时不同的对象,那么最终的结果就是一个新的Expression对象。就如上面VisitParameter方法实现一样。它直接返回我们定义的ParameterExpression对象。
通过上面分析,ParameterExpression类的作用是将一个表达式里的所有ParameterExpression替换成我们指定的新对象,这样就可以解决之前参数不一致的情况。所以我们And和Or方法的实现就是将两个表达式树参数替换成我们首先定义好的参数表达式。具体的实现方式如下所示:
public static class SpecExprExtensions { public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> one) { var candidateExpr = one.Parameters[0]; var body = Expression.Not(one.Body); return Expression.Lambda<Func<T, bool>>(body, candidateExpr); } public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> one, Expression<Func<T, bool>> another) { // 首先定义好一个ParameterExpression var candidateExpr = Expression.Parameter(typeof (T), "candidate"); var parameterReplacer = new ParameterReplacer(candidateExpr); // 将表达式树的参数统一替换成我们定义好的candidateExpr var left = parameterReplacer.Replace(one.Body); var right = parameterReplacer.Replace(another.Body); var body = Expression.And(left, right); return Expression.Lambda<Func<T, bool>>(body, candidateExpr); } public static Expression<Func<T, bool>> Or<T>( this Expression<Func<T, bool>> one, Expression<Func<T, bool>> another) { var candidateExpr = Expression.Parameter(typeof (T), "candidate"); var parameterReplacer = new ParameterReplacer(candidateExpr); var left = parameterReplacer.Replace(one.Body); var right = parameterReplacer.Replace(another.Body); var body = Expression.Or(left, right); return Expression.Lambda<Func<T, bool>>(body, candidateExpr); } }
到此,我们就完成了规约模式对Linq支持的轻量实现了。下面让我们看看上面轻量实现是如何调用的呢?具体调用代码如下:
class Program { private static void Main(string[] args) { Demo1(); Console.Read(); } public static void Demo1() { var items = Enumerable.Range(-5, 10); Console.WriteLine("产生的数组为:{0}", string.Join(", ", items.ToArray())); Expression<Func<int, bool>> f = i => i % 2 != 0; f = f.Not().And(i => i > 0); // 通过AsQueryable成IQueryable<int>,因为IQueryable<T>的Where方法的参数要求是表达式树 foreach (var i in items.AsQueryable().Where(f)) { Console.WriteLine(i); } } }
其运行结果与前面的例子中的运行结果一样,一样成功返回了即是偶数又是正数的集合。
七、总结
到这里,规约模式的实现就结束了,后期将会在网上书店的案例中引入规约模式,dax.net的Byteart Retail案例中规约模式的实现即包括了传统实现,也包括了对Linq支持的轻量实现。开始我认为传统实现是多余的,因为你已经有了规约模式的轻量实现了,何必又有传统实现呢?这不是包括两种实现吗?后面仔细想想,这样设计也有其存在的道理,因为对于一些逻辑复杂的规约实现,我们可以新建一个具体的规约类,但对于一些简单和仅使用一次的规约逻辑,就可以直接用表达式树来代替,就不需要单独为该段逻辑单独新建一个具体的规约类。这样的实现就如同,有了匿名方法和Lambda表达式,是不是委托就可以不需要了。显然不是的,所以我在我的网上书店案例中也将会引入这两种实现,让用户可以灵活选择这两种方式。在下一专题,我继续介绍一个前期准备的内容,即工作单元模式(Unit Of Work,即UOW)。
本专题的所有源码下载:SpecificationPatternDemo.zip