规约模式(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篇博文的铺垫之后,下面就具体看看如何把这两种模式引入到之前的网上书店案例里. 二.规约模式的引入 在第三专题我们已经详细介绍了 ...
随机推荐
- vue计算属性详解——小白速会
一.什么是计算属性 模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的.在模板中放入太多的逻辑会让模板过重且难以维护.例如: <div id="example"> ...
- 游戏安全有多重要?——GAME-TECH游戏开发者技术沙龙
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由云+社区运营团队发布在腾讯云+社区 腾讯云GAME-TECH沙龙继3月深圳站后,将于4月13日来到北京站,与游戏厂商和游戏开发者,畅聊 ...
- 从Firefox升级说学习方法
今天早上,打开PortableAPPs时,它提示我升级FireFox,跟往常一样我没考虑就升级了. 打开Firefox 57神速,很是惊喜,打开后发现悲剧了,自己(通过下载插件)定制的功能都不能使用了 ...
- LeetCode---Container With Most Water(11)
Description: Given n non-negative integers a1, a2, ..., an, where each represents a point at coordin ...
- c++第0次作业
1.你认为大学的学习生活.同学关系.师生应该是怎样? 随着大学生活的慢慢到来,我开始领悟到大学并不是自由的天堂,相反,我们更加的走进社会这个牢笼.在这个牢笼中有着从前的我们并不需要在意和考虑的规则与问 ...
- Python实现栈
栈的操作 Stack() 创建一个新的空栈 push(item) 添加一个新的元素item到栈顶 pop() 弹出栈顶元素 peek() 返回栈顶元素 is_empty() 判断栈是否为空 size( ...
- sys模块的使用
import sys,time ''' if sys.argv[1]=='sleepy': print('nongsi') else: print('....')''' #进度条 for i in r ...
- 第九条:覆盖equals方法时总要覆盖hashCode方法
Object类的hashCode方法: public native int hashCode(); 是一个本地方法. 其中这个方法的主要注释如下: Whenever it is invoked o ...
- 安装Loadrunner 11.0时,弹出缺少2.8 sp1组件--解决方案(win7)
这是因为注册表缺少FullInstallVer和Version,归根到底是madc安装的的问题 以下是解决方法: 1.运行regedit,打开注册表,进入HKEY_LOCAL_MACHINE\SOFT ...
- JAVA_SE基础——44.抽象类的练习
抽象类要注意的细节: 1. 如果一个函数没有方法体,那么该函数必须要使用abstract修饰,把该函数修饰成抽象 的函数..2. 如果一个类出现了抽象的函数,那么该类也必须 使用abstract修饰. ...