[关闭]
@muyanfeixiang 2016-03-16T13:55:27.000000Z 字数 4470 阅读 2081

规约模式Specification Pattern

未分类


什么是规约模式

规约模式允许我们将一小块领域知识封装到一个单元中,即规约,然后可以在code base中对其进行复用。

它可以用来解决在查询中泛滥着GetBySomething方法的问题,以及对查询条件的组合和复用。
举个例子

  1. public class Movie : Entity
  2. {
  3. public string Name { get; }
  4. public DateTime ReleaseDate { get; }
  5. public MpaaRating MpaaRating { get; }
  6. public string Genre { get; }
  7. public double Rating { get; }
  8. }
  9. public enum MpaaRating
  10. {
  11. G,
  12. PG13,
  13. R
  14. }

这样如果按不同的条件来查询,就会出现如下代码

  1. public class MovieRepository
  2. {
  3. public IReadOnlyList<Movie> GetByReleaseDate(DateTime maxReleaseDate) { }
  4. public IReadOnlyList<Movie> GetByRating(double minRating) { }
  5. public IReadOnlyList<Movie> GetByGenre(string genre) { }
  6. }

如果我们想要根据多个条件来查询情况就会变得稍微负责一点,如下

  1. public class MovieRepository
  2. {
  3. public IReadOnlyList<Movie> Find(
  4. DateTime? maxReleaseDate = null,
  5. double minRating = 0,
  6. string genre = null)
  7. {
  8. /* … */
  9. }
  10. }

而且有时,我们需要在内存中筛选数据,有时我们需要在sql中筛选数据,这样就会出现如下两种写法

  1. //内存中筛选数据
  2. public Result BuyChildTicket(int movieId)
  3. {
  4. Movie movie = _repository.GetById(movieId);
  5. if (movie.MpaaRating != MpaaRating.G)
  6. return Error(“The movie is not eligible for children”);
  7. return Ok();
  8. }
  9. //数据库中筛选数据
  10. public class MovieRepository
  11. {
  12. public IReadOnlyList<Movie> FindMoviesForChildren()
  13. {
  14. return db
  15. .Where(x => x.MpaaRating == MpaaRating.G)
  16. .ToList();
  17. }
  18. }

这样子就违反了DRY原则,因为领域相关的信息(儿童电影)就出现在了两个位置。此时,我们可以通过规约模式来解决该问题。我们引入一个新的类来去甄别不同类型的电影。
这样不仅移除了重复的领域信息,还可以组合多个规约。
规约模式可以在如下三个场景使用

原始实现

首先给出最原始的实现方式。这种方式依赖于c#的expression。如下(这也是目前我在项目直接想到使用的方式)

  1. // Controller
  2. public void SomeMethod()
  3. {
  4. Expression<Func<Movie, bool>> expression = m => m.MpaaRating == MpaaRating.G;
  5. bool isOk = expression.Compile().Invoke(movie); // Exercising a single movie
  6. var movies = _repository.Find(expression); // Getting a list of movies
  7. //如上可以直接简写为如下一句代码
  8. movies = _repository.Find(m => m.MpaaRating == MpaaRating.G);
  9. }
  10. // Repository
  11. public IReadOnlyList<Movie> Find(Expression<Func<Movie, bool>> expression)
  12. {
  13. return db
  14. .Where(expression)
  15. .ToList();
  16. }

这种方式,解决了GetBySomething的情况,但是领域信息是不能复用的。m => m.MpaaRating == MpaaRating.G这样的代码可能会在应用中被复制多次。
一种改进的实现方式,是使用泛型规约类,如下。

  1. public class GenericSpecification<T>
  2. {
  3. public Expression<Func<T, bool>> Expression { get; }
  4. public GenericSpecification(Expression<Func<T, bool>> expression)
  5. {
  6. Expression = expression;
  7. }
  8. public bool IsSatisfiedBy(T entity)
  9. {
  10. return Expression.Compile().Invoke(entity);
  11. }
  12. }
  13. public void SomeMethod()
  14. {
  15. var specification = new GenericSpecification<Movie>(
  16. m => m.MpaaRating == MpaaRating.G);
  17. bool isOk = specification.IsSatisfiedBy(movie);
  18. var movies = _repository.Find(specification);
  19. }
  20. public IReadOnlyList<Movie> Find(GenericSpecification<Movie> specification)
  21. {
  22. return db
  23. .Where(specification.Expression)
  24. .ToList();
  25. }

这里呢,问题依然如上,仍旧没解决m => m.MpaaRating == MpaaRating.G不方便复用。

强类型规约

这种方式,我们将领域信息硬编码进规约内,外部不能或者不太可能改变。

  1. public abstract class Specification<T>
  2. {
  3. public abstract Expression<Func<T, bool>> ToExpression();
  4. public bool IsSatisfiedBy(T entity)
  5. {
  6. Func<T, bool> predicate = ToExpression().Compile();
  7. return predicate(entity);
  8. }
  9. }
  10. public class MpaaRatingAtMostSpecification : Specification<Movie>
  11. {
  12. private readonly MpaaRating _rating;
  13. public MpaaRatingAtMostSpecification(MpaaRating rating)
  14. {
  15. _rating = rating;
  16. }
  17. public override Expression<Func<Movie, bool>> ToExpression()
  18. {
  19. return movie => movie.MpaaRating <= _rating;
  20. }
  21. }

这样我们使得领域信息更容易复用,而且在创建其他规约时,也不需要重复原来的规约。而且非常方便对规约进行组合,如And、Or和Not。如下

  1. public abstract class Specification<T>
  2. {
  3. public Specification<T> And(Specification<T> specification)
  4. {
  5. return new AndSpecification<T>(this, specification);
  6. }
  7. // And also Or and Not methods
  8. }
  9. public class AndSpecification<T> : Specification<T>
  10. {
  11. private readonly Specification<T> _left;
  12. private readonly Specification<T> _right;
  13. public AndSpecification(Specification<T> left, Specification<T> right)
  14. {
  15. _right = right;
  16. _left = left;
  17. }
  18. public override Expression<Func<T, bool>> ToExpression()
  19. {
  20. Expression<Func<T, bool>> leftExpression = _left.ToExpression();
  21. Expression<Func<T, bool>> rightExpression = _right.ToExpression();
  22. BinaryExpression andExpression = Expression.AndAlso(
  23. leftExpression.Body, rightExpression.Body);
  24. return Expression.Lambda<Func<T, bool>>(
  25. andExpression, leftExpression.Parameters.Single());
  26. }
  27. }
  28. //如下扩展方法,是用来处理AndAlso中的表达式树参数的
  29. public static class ExpressionExt
  30. {
  31. static Expression<Func<T, bool>> AndAlso<T>(
  32. this Expression<Func<T, bool>> expr1,
  33. Expression<Func<T, bool>> expr2)
  34. {
  35. // need to detect whether they use the same
  36. // parameter instance; if not, they need fixing
  37. ParameterExpression param = expr1.Parameters[0];
  38. if (ReferenceEquals(param, expr2.Parameters[0]))
  39. {
  40. // simple version
  41. return Expression.Lambda<Func<T, bool>>(
  42. Expression.AndAlso(expr1.Body, expr2.Body), param);
  43. }
  44. // otherwise, keep expr1 "as is" and invoke expr2
  45. return Expression.Lambda<Func<T, bool>>(
  46. Expression.AndAlso(
  47. expr1.Body,
  48. Expression.Invoke(expr2, param)), param);
  49. }
  50. }

这样,我们就可以组合规约了,如下

  1. var gRating = new MpaaRatingAtMostSpecification(MpaaRating.G);
  2. var goodMovie = new GoodMovieSpecification();
  3. var repository = new MovieRepository();
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注