实际上,它主要是用来对实体和其它业务对象构造可重用的过滤器。

    3.8.2 示例

    在这节,我们会了解到规约模式的必要性。这节中说到的都是通用的与ABP的实现无关。

    假设有个统计客户数量的方法;如下所示:

    你可能想以过滤的方式来取得客户的数量。例如:你想取得高端客户(资产超过$100,000)的数量,或者通过注册年份来过滤客户。那么你要创建其它的方法来取得这些数据:GetPremiumCustomerCount(),GetCustomerCountRegisteredInYear(int year),GetPremiumCustomerCountRegisteredInYear(int year)等等。你可能还有其它更多的条件,为每个可能的条件来创建一个组合这是不可能的。

    规约模式 将是解决这类问题的一种好方案。我们可以创建一个传入参数为过滤条件的方法:

    1. {
    2. private readonly IRepository<Customer> _customerRepository;
    3. public CustomerManager(IRepository<Customer> customerRepository)
    4. {
    5. _customerRepository = customerRepository;
    6. }
    7. public int GetCustomerCount(ISpecification<Customer> spec)
    8. {
    9. var customers = _customerRepository.GetAllList();
    10. var customerCount = 0;
    11. foreach (var customer in customers)
    12. {
    13. if (spec.IsSatisfiedBy(customer))
    14. customerCount++;
    15. }
    16. }
    17. return customerCount;
    18. }
    19. }

    这样,我们就可以传入实现了 ISpecification\ 接口的对象作为参数;如下所示:

    1. public interface ISpecification<T>
    2. {
    3. bool IsSatisfiedBy(T obj);
    4. }

    我们可以对某个客户调用 IsSatisfiedBy 方法来测试它是否符合条件。这样,我们可以使用同样的GetCustomerCount来做不同的过滤,而不用改变方法本身

    虽然这种解决方案在理论上很好,但在c#中应该改进它,以便更好地工作。例如:从数据库中取得所有的客户来检查他们是否符合条件,这是非常没有效率的做法。在下节,我们会了解到ABP中的实现克服了该类问题。

    添加了一个 ToExpression() 方法来返回表达式,这可以更好的与 IQueryable和表达式树 整合。因此,在数据库级别上,我们可以很容易的传递一个规约给仓储来应用过滤条件。

    一般我们不是直接实现 ISpecification\ 接口,而是继承 Specification\ 类。规约类已经的实现了 IsSatisfiedBy 方法。所以,我们只需要定义 ToExpression 方法。让我们来创建一些规约类:

    1. //假设客户资产超过$100,000+的是高级客户
    2. public class PremiumCustomerSpecification : Specification<Customer>
    3. {
    4. public override Expression<Func<Customer, bool>> ToExpression()
    5. {
    6. return (customer) => (customer.Balance >= 100000);
    7. }
    8. }
    9. //参数化的规约示例
    10. public class CustomerRegistrationYearSpecification : Specification<Customer>
    11. {
    12. public int Year { get; }
    13. {
    14. Year = year;
    15. }
    16. public override Expression<Func<Customer, bool>> ToExpression()
    17. {
    18. return (customer) => (customer.CreationYear == Year);
    19. }
    20. }

    如你所见,我们只是简单的实现了 lambda表达式 来定义规约。让我们来使用这些规约来取得客户的数量:

    1. count = customerManager.GetCustomerCount(new PremiumCustomerSpecification());
    2. count = customerManager.GetCustomerCount(new CustomerRegistrationYearSpecification(2017));

    3.8.4 使用规约与仓储

    现在,我们可以 优化 CustomerManager来 应用在数据库中的过滤条件

    就是这么任性。我们可以传入任意规约给仓储,因为可以与作为过滤条件的表达式一起工作。在这个例子中,CustomerManager不是必需的,因为我们可以直接的使用仓储和规约来执行数据库查询操作。但是,如果我们想对某些客户执行一个业务操作。在这种情况下,我们可以使用规约和领域服务一起来指定客户。

    规约的一个强大功能是可以组合这些扩展方法:And,Or,Not以及AndNot。例如:

    1. var count = customerManager.GetCustomerCount(new PremiumCustomerSpecification().And(new CustomerRegistrationYearSpecification(2017)));

    我们甚至可以从已有的规约中创建一个新的规约类:

    1. public class NewPremiumCustomersSpecification : AndSpecification<Customer>
    2. {
    3. public NewPremiumCustomersSpecification()
    4. : base(new PremiumCustomerSpecification(), new CustomerRegistrationYearSpecification(2017))
    5. {
    6. }
    7. }

    AndSpecificationSpecification 的子类,只有当规约的两边都匹配的时候才满足条件。那么,我么可以像其它规约一样来使用NewPremiumCustomersSpecification:

    3.8.6 讨论

    1. var count = _customerRepository.Count(c => c.Balance > 100000 && c.CreationYear == 2017);

    ABP的仓储支持表达式用法,这是完全有效的用法。你可以不在应用中定义或者使用规约,你可以直接使用Linq表达式。那么,规约的意义是什么?为什么要使用规约以及什么时候我们该使用规约?

    什么时候使用?

    使用规约的好处:

    • 重用:你在应用程序的很多地方都需要过滤高级客户。如果你使用Linq表达式而不创建规约;如果你迟些时候改变了 “高级客户” 的定义,那会发生什么?(例如:你想将客户的资产从$100,000调到$250,000,并且你还要添加其它过滤条件:客户年龄超过30岁)。如果你使用了规约,你仅仅只需要改变一个类。如果你使用Linq表达式,那么你需要在很多地方来改变表达式过滤。

    • 组合:你可以组合多个规约来创建一个新的规约。这是另一种可重用的类型。

    • 有意义的命名:相较于一个复杂的表达式,PremiumCustomerSpecification能够更好的表达它的意图。如果在你的业务中使用了一个有意义的表达式,那么请考虑使用规约。

    • 测试:可以对规约分别测试,这样测试更简单。

    什么时候不使用?

    • 无业务表达式:对于与业务不相关表达式和操作,你可以不使用规约。

    • 报表:如果你只是创建报表,那么没必要使用规约,可以直接的使用IQuerable。实际上,对于报表你可以使用纯SQL,视图或者其它工具。DDD对于报表不是十分关注;从性能的角度看,更应该优化查询来获取底层数据。