规约模式(Specification Pattern)
一、引言
最近在看一个项目的源码时(DDD),对里面的一些设计思想和设计思路有了一些疑问。当看到(Repository层)中使用了
spec.SatisfiedBy()时,感觉有点懵。于是在项目中搜了搜,在项目的Domain层,即引入了规约模式,因为之前较少接触这个,于是今天便去了解了一下。当然园子里面有挺多人对这个模式进行了自己的理解和解释。希望我能够解释的清楚,如有误,多多包涵。欢迎留言指正。
二、名词释义
- Specification pattern is a pattern that allows us to encapsulate some piece of domain knowledge into a single unit – specification – and reuse it in different parts of the code base.
 
这个是我在一个一篇文章中看到的对于 Specification Pattern 的一个解释。大致的意思应该是这样,规约模式是为了让我们把领域中可能都会通用的东西 抽离出来独立放在一个地方,给其他地方复用。- 园子里的还有一个人(原名:陈晴阳。博客园名:dax.net)的解释是:规约模式解耦了仓储操作与查询条件。大家有兴趣可以参考这篇博文。
 - The Specification pattern is a very powerful design pattern which can be used to remove a lot of cruft from a class's interface while decreasing coupling and increasing extensibility. It's primary use is to select a subset of objects based on some criteria, and to refresh the selection at various times.The Specification Pattern
 
三、如何使用
下面是一篇外文,讲的如何实现,讲的挺好:来源:http://enterprisecraftsmanship.com/2016/02/08/specification-pattern-c-implementation/
Use cases for this pattern are best expressed with an example. Let’s say we have the following class in our domain model:
public class Movie : Entity
{
    public string Name { get; }
    public DateTime ReleaseDate { get; }
    public MpaaRating MpaaRating { get; }
    public string Genre { get; }
    public double Rating { get; }
}
public enum MpaaRating
{
    G,
    PG13,
    R
}
Now, let’s assume that users want to find some relatively new movies to watch. To implement this, we can add a method to a repository class, like this:
public class MovieRepository
{
    public IReadOnlyList<Movie> GetByReleaseDate(DateTime minReleaseDate)
    {
        /* … */
    }
}
If we need to search by rating or genre, we can introduce other methods as well:
public class MovieRepository
{
    public IReadOnlyList<Movie> GetByReleaseDate(DateTime maxReleaseDate) { }
    public IReadOnlyList<Movie> GetByRating(double minRating) { }
    public IReadOnlyList<Movie> GetByGenre(string genre) { }
}
Things get a bit more complicated when we decide to combine the search criteria, but we are still in a good shape. We can introduce a single Find method which would handle all possible criteria and return a consolidated search result:
public class MovieRepository
{
    public IReadOnlyList<Movie> Find(
        DateTime? maxReleaseDate = null,
        double minRating = 0,
        string genre = null)
    {
        /* … */
    }
}
And of course, we can always add other criteria to the method as well.
Problems arise when we need to not only search for the data in the database but also validate it in the memory. For example, we might want to check that a certain movie is eligible for children before we sell a ticket to it, so we introduce a validation, like this:
public Result BuyChildTicket(int movieId)
{
    Movie movie = _repository.GetById(movieId);
    if (movie.MpaaRating != MpaaRating.G)
        return Error(“The movie is not eligible for children”);
    return Ok();
}
If we also need to look into the database for all movies that meet the same criterion, we have to introduce a method similar to the following:
public class MovieRepository
{
    public IReadOnlyList<Movie> FindMoviesForChildren()
    {
        return db
            .Where(x => x.MpaaRating == MpaaRating.G)
            .ToList();
    }
}
The issue with this code is that it violates the DRY principle as the domain knowledge about what to consider a kids movie is now spread across 2 locations: the BuyChildTicket method and MovieRepository. That is where the Specification pattern can help us. We can introduce a new class which knows exactly how to distinguish different kinds of movies. We then can reuse this class in both scenarios:
public Result BuyChildTicket(int movieId)
{
    Movie movie = _repository.GetById(movieId);
    var spec = new MovieForKidsSpecification();
    if (!spec.IsSatisfiedBy(movie))
        return Error(“The movie is not eligible for children”);
    return Ok();
}
public class MovieRepository
{
    public IReadOnlyList<Movie> Find(Specification<Movie> specification)
    {
        /* … */
    }
}
Not only does this approach removes domain knowledge duplication, it also allows for combining multiple specifications. That, in turn, helps us easily set up quite complex search and validation criteria.
There are 3 main use cases for the Specification pattern:
- Looking up data in the database. That is finding records that match the specification we have in hand.
 - Validating objects in the memory. In other words, checking that an object we retrieved or created fits the spec.
 - Creating a new instance that matches the criteria. This is useful in scenarios where you don’t care about the actual content of the instances, but still need it to have certain attributes.
We will discuss the first 2 use cases as they are the most common in my experience. 
Naive implementation
We’ll start implementing the specification pattern with a naive version first and will then move forward to a better one.
The first solution that comes to mind when you face the problem described above is to use C# expressions. To a great extent, they themselves are an implementation of the specification pattern. We can easily define one in code and use it in both scenarios, like this:
// Controller
public void SomeMethod()
{
    Expression<Func<Movie, bool>> expression = m => m.MpaaRating == MpaaRating.G;
    bool isOk = expression.Compile().Invoke(movie); // Exercising a single movie
    var movies = _repository.Find(expression); // Getting a list of movies
}
// Repository
public IReadOnlyList<Movie> Find(Expression<Func<Movie, bool>> expression)
{
    return db
        .Where(expression)
        .ToList();
}
The problem with this approach, however, is that while we do gather the domain knowledge regarding how to categorize kids movies in a single place (expression variable in our case), the abstraction we’ve chosen isn’t a good fit. Variables are by no means a suitable place for such important information. The domain knowledge represented in such a way is hard to reuse and tends to be duplicated across the whole application because of that. Ultimately, we end up with the same issue we started off with.
A variation of this naive implementation is introducing a generic specification class:
public class GenericSpecification<T>
{
    public Expression<Func<T, bool>> Expression { get; }
    public GenericSpecification(Expression<Func<T, bool>> expression)
    {
        Expression = expression;
    }
    public bool IsSatisfiedBy(T entity)
    {
        return Expression.Compile().Invoke(entity);
    }
}
// Controller
public void SomeMethod()
{
    var specification = new GenericSpecification<Movie>(
        m => m.MpaaRating == MpaaRating.G);
    bool isOk = specification.IsSatisfiedBy(movie); // Exercising a single movie
    var movies = _repository.Find(specification); // Getting a list of movies
}
// Repository
public IReadOnlyList<Movie> Find(GenericSpecification<Movie> specification)
{
    return db
        .Where(specification.Expression)
        .ToList();
}
This version has essentially the same drawbacks, the only difference is that here we have a wrapper class on top of the expression. Still, in order to reuse such specification properly, we have to create a single instance of it once and then share this instance across our code base somehow. This design doesn’t help much with DRY.
That leads us to an important conclusion: generic specifications are a bad practice. If a specification allows you to indicate an arbitrary condition, it becomes just a container for the information which is passed to it by its client and doesn’t solve the underlying problem of domain knowledge encapsulation. Such specifications simply don’t contain any knowledge themselves.
Strongly-typed specifications
So how can we overcome the problem? The solution here is to use strongly-typed specifications. That is specifications in which we hard code the domain knowledge, with little or no possibility to alter it from the outside.
Here’s how we can implement it in practice:
public abstract class Specification<T>
{
    public abstract Expression<Func<T, bool>> ToExpression();
    public bool IsSatisfiedBy(T entity)
    {
        Func<T, bool> predicate = ToExpression().Compile();
        return predicate(entity);
    }
}
public class MpaaRatingAtMostSpecification : Specification<Movie>
{
    private readonly MpaaRating _rating;
    public MpaaRatingAtMostSpecification(MpaaRating rating)
    {
        _rating = rating;
    }
    public override Expression<Func<Movie, bool>> ToExpression()
    {
        return movie => movie.MpaaRating <= _rating;
    }
}
// Controller
public void SomeMethod()
{
    var gRating = new MpaaRatingAtMostSpecification(MpaaRating.G);
    bool isOk = gRating.IsSatisfiedBy(movie); // Exercising a single movie
    IReadOnlyList<Movie> movies = repository.Find(gRating); // Getting a list of movies
}
// Repository
public IReadOnlyList<T> Find(Specification<T> specification)
{
    using (ISession session = SessionFactory.OpenSession())
    {
        return session.Query<T>()
            .Where(specification.ToExpression())
            .ToList();
    }
}
With this approach, we lift the domain knowledge to the class level making it much easier to reuse. No need to keep track of spec instances anymore: creating additional specification objects doesn’t lead to the domain knowledge duplication, so we can do it freely.
Also, it’s really easy to combine the specifications using And, Or, and Not methods. Here’s how we can do that:
public abstract class Specification<T>
{
    public Specification<T> And(Specification<T> specification)
    {
        return new AndSpecification<T>(this, specification);
    }
    // And also Or and Not methods
}
public class AndSpecification<T> : Specification<T>
{
    private readonly Specification<T> _left;
    private readonly Specification<T> _right;
    public AndSpecification(Specification<T> left, Specification<T> right)
    {
        _right = right;
        _left = left;
    }
    public override Expression<Func<T, bool>> ToExpression()
    {
        Expression<Func<T, bool>> leftExpression = _left.ToExpression();
        Expression<Func<T, bool>> rightExpression = _right.ToExpression();
        BinaryExpression andExpression = Expression.AndAlso(
            leftExpression.Body, rightExpression.Body);
        return Expression.Lambda<Func<T, bool>>(
            andExpression, leftExpression.Parameters.Single());
    }
}
And this is a usage example:
var gRating = new MpaaRatingAtMostSpecification(MpaaRating.G);`
var goodMovie = new GoodMovieSpecification();`
var repository = new MovieRepository();`
IReadOnlyList<Movie> movies = repository.Find(gRating.And(goodMovie));
You can find the full source code and usage examples on Github.
Returning IQueryable from a repository
A question that is somewhat related to the specification pattern is: can repositories just return an IQueryable? Wouldn’t it be easier to allow clients to query data from the backing store the way they want? For example, we could add a method to the repository like this:
// Repository
public IQueryable<T> Find()
{
    return session.Query<T>();
}
And then use it in a controller specifying the actual criteria ad hoc:
// Controller
public void SomeMethod()
{
    List<Movie> movies = _repository.Find()
        .Where(movie => movie.MpaaRating == MpaaRating.G)
        .ToList();
}
This approach has essentially the same drawback as our initial specification pattern implementation: it encourages us to violate the DRY principle by duplicating the domain knowledge. This technique doesn’t offer us anything in terms of consolidating it in a single place.
The second drawback here is that we are getting database notions leaking out of repositories. The implementation of IQueryable highly depends on what LINQ provider is used behind the scene, so the client code should be aware that there potentially are queries which can’t be compiled into SQL.
And finally, we are also getting a potential LSP violation. IQueryables are evaluated lazily, so we need to keep the underlying connection opened during the whole business transaction. Otherwise, the method will blow up with an exception. By the way, an implementation with IEnumerables have essentially the same problem, so the best way to overcome this issue is to return IReadOnlyList or IReadOnlyCollection interfaces.
Source code
Full source code for the specification pattern implementation in C#
Summary
- Don’t use C# expressions as a Specification pattern implementation, they don’t allow you to actually gather the domain knowledge into a single authoritative source.
 - Don’t return IQueryable from repositories, it brings up issues with DRY and LSP violation and leaking database concerns to the application logic.
 
参考文章
- http://enterprisecraftsmanship.com/2016/02/08/specification-pattern-c-implementation/
 - http://www.cnblogs.com/daxnet/archive/2010/07/19/1780764.html
 - The Specification Pattern: A Primer(https://matt.berther.io/2005/03/25/the-specification-pattern-a-primer/)
 
规约模式(Specification Pattern)的更多相关文章
- 规约模式(Specification Pattern)
		
前期准备之规约模式(Specification Pattern) 一.前言 在专题二中已经应用DDD和SOA的思想简单构建了一个网上书店的网站,接下来的专题中将会对该网站补充更多的DDD的内容.本专题 ...
 - [.NET领域驱动设计实战系列]专题三:前期准备之规约模式(Specification Pattern)
		
一.前言 在专题二中已经应用DDD和SOA的思想简单构建了一个网上书店的网站,接下来的专题中将会对该网站补充更多的DDD的内容.本专题作为一个准备专题,因为在后面一个专题中将会网上书店中的仓储实现引入 ...
 - 规约模式Specification Pattern
		
什么是规约模式 规约模式允许我们将一小块领域知识封装到一个单元中,即规约,然后可以在code base中对其进行复用. 它可以用来解决在查询中泛滥着GetBySomething方法的问题,以及对查询条 ...
 - 规约模式Specification的学习
		
最近一直在看DDD开发 规约似乎用得很普遍. 但是还是理解不了.所以记录下学习的进度.- 规约(Specification)模式 目的:查询语句和查询条件的分离 写了一个关于规约的模拟小程序 cla ...
 - 规格模式(Specification Pattern)
		
本文节选自<设计模式就该这样学> 1 规格模式的定义 规格模式(Specification Pattern)可以认为是组合模式的一种扩展.很多时候程序中的某些条件决定了业务逻辑,这些条件就 ...
 - step_by_step_ABP规约模式
		
一段时间没有在github 上浏览ABP项目,几天前看到ABP新增规约模式,开始了解并学习文档 记录一下 Introduction 介绍 Specification pattern is a pa ...
 - 设计模式:规约模式(Specification-Pattern)
		
"其实地上本没有路,走的人多了,也便成了路"--鲁迅<故乡> 这句话很好的描述了设计模式的由来.前辈们通过实践和总结,将优秀的编程思想沉淀成设计模式,为开发者提供了解决 ...
 - 生产环境下实践DDD中的规约模式
		
最近的开发工作涉及到两个模块“任务”和“日周报”.关系是日周报消费任务,因为用户在写日周报的时候,需要按一定的规则筛选当前用户的任务,作为日周报的一部分提交.整个项目采用类似于Orchard那种平台加 ...
 - [.NET领域驱动设计实战系列]专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现
		
一.前言 在前面2篇博文中,我分别介绍了规约模式和工作单元模式,有了前面2篇博文的铺垫之后,下面就具体看看如何把这两种模式引入到之前的网上书店案例里. 二.规约模式的引入 在第三专题我们已经详细介绍了 ...
 
随机推荐
- Python下载图片小程序
			
欢迎大侠们指正批评 思路: 1.引入相关的python文件(import re import urllib) 2.读取对应网页的html文件(使用 urllib) def getHtml(url): ...
 - linux,windows,ubuntu下git安装与使用
			
ubuntu下git安装与使用:首先应该检查本地是否已经安装了git ,如果没有安装的话,在命令模式下输入 sudo apt-get install git 进行安装 输入git命令查看安装状态及常用 ...
 - C语言的第一次作业总结
			
PTA实验作业 题目一:温度转换 本题要求编写程序,计算华氏温度150°F对应的摄氏温度.计算公式:C=5×(F−32)/9,式中:C表示摄氏温度,F表示华氏温度,输出数据要求为整型. 1.实验代码: ...
 - Flask 学习 十  博客文章
			
提交和显示博客文章 app/models.py 文章模型 class Post(db.Model): __tablename__ = 'posts' id = db.Column(db.Integer ...
 - 翻译:CREATE FUNCTION语句(已提交到MariaDB官方手册)
			
本文为mariadb官方手册:CREATE FUNCTION的译文. 原文:https://mariadb.com/kb/en/library/create-function/我提交到MariaDB官 ...
 - 数据结构与算法 —— 链表linked list(02)
			
我们继续来看链表的第二道题,来自于leetcode: 两数相加 给定两个非空链表来代表两个非负整数,位数按照逆序方式存储,它们的每个节点只存储单个数字.将这两数相加会返回一个新的链表. 你可以假设除了 ...
 - Python内置函数(55)——globals
			
英文文档: globals() Return a dictionary representing the current global symbol table. This is always the ...
 - jQuery serialize()方法获取不到数据,alert结果为空
			
网上查找,问题可能是 id有重复 经排查,没有发现重复id 解决方案 form表单中每个input框都没有name属性,添加name属性即可 若name属性与jQuery的关键字有冲突,也可导致该问题 ...
 - 论文泛读·Adversarial Learning for Neural Dialogue Generation
			
原文翻译 导读 这篇文章的主要工作在于应用了对抗训练(adversarial training)的思路来解决开放式对话生成(open-domain dialogue generation)这样一个无监 ...
 - Opencv出现“_pFirstBlock == pHead”错误的解决方法
			
先说结论: opencv链接库使用错误. 1,确认VS工程属性中,opencv的链接库路径和版本正确. VS2013应该使用vc12目录,VS2012对应vc11目录.debug版和release版要 ...