3.8 ABP领域层 - 规约模式
3.8.1 简介
规约模式 是一种特别的软件设计模式,通过链接业务规则与使用boolean逻辑来重组业务规则。
实际上,它主要是用来对实体和其它业务对象构造可重用的过滤器。
3.8.2 示例
在这节,我们会了解到规约模式的必要性。这节中说到的都是通用的与ABP的实现无关。
假设有个统计客户数量的方法;如下所示:
public class CustomerManager
{
public int GetCustomerCount()
{
//TODO...
return 0;
}
}
你可能想以过滤的方式来取得客户的数量。例如:你想取得高端客户(资产超过$100,000)的数量,或者通过注册年份来过滤客户。那么你要创建其它的方法来取得这些数据:GetPremiumCustomerCount(),GetCustomerCountRegisteredInYear(int year),GetPremiumCustomerCountRegisteredInYear(int year)等等。你可能还有其它更多的条件,为每个可能的条件来创建一个组合这是不可能的。
规约模式 将是解决这类问题的一种好方案。我们可以创建一个传入参数为过滤条件的方法:
public class CustomerManager
{
private readonly IRepository<Customer> _customerRepository;
public CustomerManager(IRepository<Customer> customerRepository)
{
_customerRepository = customerRepository;
}
public int GetCustomerCount(ISpecification<Customer> spec)
{
var customers = _customerRepository.GetAllList();
var customerCount = 0;
foreach (var customer in customers)
{
if (spec.IsSatisfiedBy(customer))
{
customerCount++;
}
}
return customerCount;
}
}
这样,我们就可以传入实现了 ISpecification\
public interface ISpecification<T>
{
bool IsSatisfiedBy(T obj);
}
我们可以对某个客户调用 IsSatisfiedBy 方法来测试它是否符合条件。这样,我们可以使用同样的GetCustomerCount来做不同的过滤,而不用改变方法本身。
虽然这种解决方案在理论上很好,但在c#中应该改进它,以便更好地工作。例如:从数据库中取得所有的客户来检查他们是否符合条件,这是非常没有效率的做法。在下节,我们会了解到ABP中的实现克服了该类问题。
3.8.3 创建规约类
在ABP中定义了 ISpecification 接口,如下所示:
public interface ISpecification<T>
{
bool IsSatisfiedBy(T obj);
Expression<Func<T, bool>> ToExpression();
}
添加了一个 ToExpression() 方法来返回表达式,这可以更好的与 IQueryable和表达式树 整合。因此,在数据库级别上,我们可以很容易的传递一个规约给仓储来应用过滤条件。
一般我们不是直接实现 ISpecification\
//假设客户资产超过$100,000+的是高级客户
public class PremiumCustomerSpecification : Specification<Customer>
{
public override Expression<Func<Customer, bool>> ToExpression()
{
return (customer) => (customer.Balance >= 100000);
}
}
//参数化的规约示例
public class CustomerRegistrationYearSpecification : Specification<Customer>
{
public int Year { get; }
public CustomerRegistrationYearSpecification(int year)
{
Year = year;
}
public override Expression<Func<Customer, bool>> ToExpression()
{
return (customer) => (customer.CreationYear == Year);
}
}
如你所见,我们只是简单的实现了 lambda表达式 来定义规约。让我们来使用这些规约来取得客户的数量:
count = customerManager.GetCustomerCount(new PremiumCustomerSpecification());
count = customerManager.GetCustomerCount(new CustomerRegistrationYearSpecification(2017));
3.8.4 使用规约与仓储
现在,我们可以 优化 CustomerManager来 应用在数据库中的过滤条件:
public class CustomerManager
{
private readonly IRepository<Customer> _customerRepository;
public CustomerManager(IRepository<Customer> customerRepository)
{
_customerRepository = customerRepository;
}
public int GetCustomerCount(ISpecification<Customer> spec)
{
return _customerRepository.Count(spec.ToExpression());
}
}
就是这么任性。我们可以传入任意规约给仓储,因为仓储可以与作为过滤条件的表达式一起工作。在这个例子中,CustomerManager不是必需的,因为我们可以直接的使用仓储和规约来执行数据库查询操作。但是,如果我们想对某些客户执行一个业务操作。在这种情况下,我们可以使用规约和领域服务一起来指定客户。
3.8.5 组合规约
规约的一个强大功能是可以组合这些扩展方法:And,Or,Not以及AndNot。例如:
var count = customerManager.GetCustomerCount(new PremiumCustomerSpecification().And(new CustomerRegistrationYearSpecification(2017)));
我们甚至可以从已有的规约中创建一个新的规约类:
public class NewPremiumCustomersSpecification : AndSpecification<Customer>
{
public NewPremiumCustomersSpecification()
: base(new PremiumCustomerSpecification(), new CustomerRegistrationYearSpecification(2017))
{
}
}
AndSpecification 是 Specification 的子类,只有当规约的两边都匹配的时候才满足条件。那么,我么可以像其它规约一样来使用NewPremiumCustomersSpecification:
var count = customerManager.GetCustomerCount(new NewPremiumCustomersSpecification());
3.8.6 讨论
虽然规约模式比C#的lambda表达式老旧。一些开发者认为可以不用使用了,我们可以直接的传入表达式给仓储或者领域服务;如下所示:
var count = _customerRepository.Count(c => c.Balance > 100000 && c.CreationYear == 2017);
ABP的仓储支持表达式用法,这是完全有效的用法。你可以不在应用中定义或者使用规约,你可以直接使用Linq表达式。那么,规约的意义是什么?为什么要使用规约以及什么时候我们该使用规约?
什么时候使用?
使用规约的好处:
重用:你在应用程序的很多地方都需要过滤高级客户。如果你使用Linq表达式而不创建规约;如果你迟些时候改变了 “高级客户” 的定义,那会发生什么?(例如:你想将客户的资产从$100,000调到$250,000,并且你还要添加其它过滤条件:客户年龄超过30岁)。如果你使用了规约,你仅仅只需要改变一个类。如果你使用Linq表达式,那么你需要在很多地方来改变表达式过滤。
组合:你可以组合多个规约来创建一个新的规约。这是另一种可重用的类型。
有意义的命名:相较于一个复杂的表达式,PremiumCustomerSpecification能够更好的表达它的意图。如果在你的业务中使用了一个有意义的表达式,那么请考虑使用规约。
测试:可以对规约分别测试,这样测试更简单。
什么时候不使用?
无业务表达式:对于与业务不相关表达式和操作,你可以不使用规约。
报表:如果你只是创建报表,那么没必要使用规约,可以直接的使用IQuerable。实际上,对于报表你可以使用纯SQL,视图或者其它工具。DDD对于报表不是十分关注;从性能的角度看,更应该优化查询来获取底层数据。