zoukankan      html  css  js  c++  java
  • 规约模式(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.

    参考文章

  • 相关阅读:
    推荐一款Notepad++主题Dracula
    一个小工具,利用php把指定目录文件递归上传到阿里云OSS
    svn2个小问题的解决
    借助Algorithmia网站API:用AI给黑白照片上色,复现记忆中的旧时光
    C++@sublime GDB调试
    C++@重载函数
    C++@语句块
    C++@命名空间(转)
    《Linux与Qt程序设计》知识框架
    多线程中的使用共享变量的问题 (转)
  • 原文地址:https://www.cnblogs.com/xiyin/p/6382230.html
Copyright © 2011-2022 走看看