规约模式(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篇博文的铺垫之后,下面就具体看看如何把这两种模式引入到之前的网上书店案例里. 二.规约模式的引入 在第三专题我们已经详细介绍了 ...
随机推荐
- MongoDB系列三(Spring集成方案).
一.前言 MongoDB是最为流行的开源文档数据库之一.Spring Data MongoDB提供了三种方式在Spring应用中使用MongoDB: 通过注解实现对象-文档映射: 使用MongoTem ...
- Java基础学习笔记十三 常用API之正则表达式、Date、DateFormat、Calendar
正则表达式 正则表达式(英语:Regular Expression,在代码中常简写为regex).正则表达式是一个字符串,使用单个字符串来描述.用来定义匹配规则,匹配一系列符合某个句法规则的字符串.在 ...
- 福州大学W班-助教总结
开学初对自己的期望 在即将到来的学期前,我希望我可以做到以下几点: 1.多参与同学的课程设计,并提出自己的见解 2.不断提高个人的专业技能,活到老学到老 3.能够及时对同学的博客进行评论,并给出有用的 ...
- 每日冲刺报告——Day1(Java-Team)
第一天报告(11.2 周四) 团队:Java-Team 成员: 章辉宇(284) 吴政楠(286) 陈阳(PM:288) 韩华颂(142) 胡志权(143) github地址:https://git ...
- 20162321王彪-实验二-Java面向对象程序设计
实验二Java面向对象程序设计 实验内容一 初步掌握单元测试和TDD 什么是单元测试:单元测试时开发者编写的一小段代码,用于检测被测代码的一个很小的,很明确的功能是否正确.执行单元测试,是为了证明某段 ...
- listview、gradview滚动到最后时,滑动至顶部
listview.gradview滑动顶端.底部的判断及底部滑动至顶端 mPhotoWall.setOnScrollListener(new AbsListView.OnScrollListener( ...
- HTTP协议以及HTTP2.0/1.1/1.0区别
HTTP协议以及HTTP2.0/1.1/1.0区别 一.简介 摘自百度百科: 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议.所 ...
- 使用 PHP 来做 Vue.js 的 SSR 服务端渲染
对于客户端应用来说,服务端渲染是一个热门话题.然而不幸的是,这并不是一件容易的事,尤其是对于不用 Node.js 环境开发的人来说. 我发布了两个库让 PHP 从服务端渲染成为可能.spatie/se ...
- crlf注入攻击
1.crlf 注入攻击. 原理:http数据包通过\r\n\r\n来分开http header何http body 实现:首先这种攻击发生在应用层,且发生在服务器返回给我们的http reponse没 ...
- ASP.NET MVC中错误处理方式
/// <summary> /// 标记了HandleError,并指明错误处理页为AboutError.aspx /// </summary> /// <returns ...