规约

规约模式用于为实体和其他业务对象定义 命名、可复用、可组合和可测试的过滤器 .

规约是领域层的一部分.

安装

这个包 已经安装 在启动模板中.所以,大多数时候你不需要手动去安装.

添加 Volo.Abp.Specifications 包到你的项目. 如果当前文件夹是你的项目的根目录(.csproj)时,你可以在命令行终端中使用 ABP CLI add package 命令:

  1. abp add-package Volo.Abp.Specifications

定义规约

假设你定义了如下的顾客实体:

  1. using System;
  2. using Volo.Abp.Domain.Entities;
  3. namespace MyProject
  4. {
  5. public class Customer : AggregateRoot<Guid>
  6. {
  7. public string Name { get; set; }
  8. public byte Age { get; set; }
  9. public long Balance { get; set; }
  10. public string Location { get; set; }
  11. }
  12. }

你可以创建一个由 Specification<Customer> 派生的新规约类.

例如:规定选择一个18岁以上的顾客

  1. using System;
  2. using System.Linq.Expressions;
  3. using Volo.Abp.Specifications;
  4. namespace MyProject
  5. {
  6. public class Age18PlusCustomerSpecification : Specification<Customer>
  7. {
  8. public override Expression<Func<Customer, bool>> ToExpression()
  9. {
  10. return c => c.Age >= 18;
  11. }
  12. }
  13. }

你只需通过定义一个lambda表达式来定义规约.

你也可以直接实现ISpecification<T>接口,但是基类Specification<T>做了大量简化.

使用规约

这里有两种常见的规约用例.

IsSatisfiedBy

IsSatisfiedBy 方法可以用于检查单个对象是否满足规约.

例如:如果顾客不满足年龄规定,则抛出异常

  1. using System;
  2. using System.Threading.Tasks;
  3. using Volo.Abp.DependencyInjection;
  4. namespace MyProject
  5. {
  6. public class CustomerService : ITransientDependency
  7. {
  8. public async Task BuyAlcohol(Customer customer)
  9. {
  10. if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer))
  11. {
  12. throw new Exception(
  13. "这位顾客不满足年龄规定!"
  14. );
  15. }
  16. //TODO...
  17. }
  18. }
  19. }

ToExpression & Repositories

ToExpression() 方法可用于将规约转化为表达式.通过这种方式,你可以使用规约在数据库查询时过滤实体.

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. using Volo.Abp.DependencyInjection;
  6. using Volo.Abp.Domain.Repositories;
  7. using Volo.Abp.Domain.Services;
  8. namespace MyProject
  9. {
  10. public class CustomerManager : DomainService, ITransientDependency
  11. {
  12. private readonly IRepository<Customer, Guid> _customerRepository;
  13. public CustomerManager(IRepository<Customer, Guid> customerRepository)
  14. {
  15. _customerRepository = customerRepository;
  16. }
  17. public async Task<List<Customer>> GetCustomersCanBuyAlcohol()
  18. {
  19. var queryable = await _customerRepository.GetQueryableAsync();
  20. var query = queryable.Where(
  21. new Age18PlusCustomerSpecification().ToExpression()
  22. );
  23. return await AsyncExecuter.ToListAsync(query);
  24. }
  25. }
  26. }

规约被正确地转换为SQL/数据库查询语句,并且在DBMS端高效执行.虽然它与规约无关,但如果你想了解有关 AsyncExecuter 的更多信息,请参阅仓储文档.

实际上,没有必要使用 ToExpression() 方法,因为规约会自动转换为表达式.这也会起作用:

  1. var queryable = await _customerRepository.GetQueryableAsync();
  2. var query = queryable.Where(
  3. new Age18PlusCustomerSpecification()
  4. );

编写规约

规约有一个强大的功能是,它们可以与AndOrNot以及AndNot扩展方法组合使用.

假设你有另一个规约,定义如下:

  1. using System;
  2. using System.Linq.Expressions;
  3. using Volo.Abp.Specifications;
  4. namespace MyProject
  5. {
  6. public class PremiumCustomerSpecification : Specification<Customer>
  7. {
  8. public override Expression<Func<Customer, bool>> ToExpression()
  9. {
  10. return (customer) => (customer.Balance >= 100000);
  11. }
  12. }
  13. }

你可以将 PremiumCustomerSpecificationAge18PlusCustomerSpecification 结合起来,查询优质成人顾客的数量,如下所示:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Volo.Abp.DependencyInjection;
  4. using Volo.Abp.Domain.Repositories;
  5. using Volo.Abp.Domain.Services;
  6. using Volo.Abp.Specifications;
  7. namespace MyProject
  8. {
  9. public class CustomerManager : DomainService, ITransientDependency
  10. {
  11. private readonly IRepository<Customer, Guid> _customerRepository;
  12. public CustomerManager(IRepository<Customer, Guid> customerRepository)
  13. {
  14. _customerRepository = customerRepository;
  15. }
  16. public async Task<int> GetAdultPremiumCustomerCountAsync()
  17. {
  18. return await _customerRepository.CountAsync(
  19. new Age18PlusCustomerSpecification()
  20. .And(new PremiumCustomerSpecification()).ToExpression()
  21. );
  22. }
  23. }
  24. }

如果你想让这个组合成为一个可复用的规约,你可以创建这样一个组合的规约类,它派生自AndSpecification:

  1. using Volo.Abp.Specifications;
  2. namespace MyProject
  3. {
  4. public class AdultPremiumCustomerSpecification : AndSpecification<Customer>
  5. {
  6. public AdultPremiumCustomerSpecification()
  7. : base(new Age18PlusCustomerSpecification(),
  8. new PremiumCustomerSpecification())
  9. {
  10. }
  11. }
  12. }

现在,你就可以向下面一样重新编写 GetAdultPremiumCustomerCountAsync 方法:

  1. public async Task<int> GetAdultPremiumCustomerCountAsync()
  2. {
  3. return await _customerRepository.CountAsync(
  4. new AdultPremiumCustomerSpecification()
  5. );
  6. }

你可以从这些例子中看到规约的强大之处.如果你之后想要更改 PremiumCustomerSpecification ,比如将余额从 100.000 修改为 200.000 ,所有查询语句和合并的规约都将受到本次更改的影响.这是减少代码重复的好方法!

讨论

虽然规约模式通常与C#的lambda表达式相比较,算是一种更老的方式.一些开发人员可能认为不再需要它,我们可以直接将表达式传入到仓储或领域服务中,如下所示:

  1. var count = await _customerRepository.CountAsync(c => c.Balance > 100000 && c.Age => 18);

自从ABP的仓储支持表达式,这是一个完全有效的用法.你不必在应用程序中定义或使用任何规约,可以直接使用表达式.

所以,规约的意义是什么?为什么或者应该在什么时候考虑去使用它?

何时使用?

使用规约的一些好处:

  • 可复用:假设你在代码库的许多地方都需要用到优质顾客过滤器.如果使用表达式而不创建规约,那么如果以后更改“优质顾客”的定义会发生什么?假设你想将最低余额从100000美元更改为250000美元,并添加另一个条件,成为顾客超过3年.如果使用了规约,只需修改一个类.如果在任何其他地方重复(复制/粘贴)相同的表达式,则需要更改所有的表达式.
  • 可组合:可以组合多个规约来创建新规约.这是另一种可复用性.
  • 命名:PremiumCustomerSpecification 更好地解释了为什么使用规约,而不是复杂的表达式.因此,如果在你的业务中使用了一个有意义的表达式,请考虑使用规约.
  • 可测试:规约是一个单独(且易于)测试的对象.

什么时侯不要使用?

  • 没有业务含义的表达式:不要对与业务无关的表达式和操作使用规约.
  • 报表:如果只是创建报表,不要创建规约,而是直接使用 IQueryable 和LINQ表达式.你甚至可以使用普通SQL、视图或其他工具生成报表.DDD不关心报表,因此从性能角度来看,查询底层数据存储的方式可能很重要.