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<Customer> 接口的对象作为参数;如下所示:

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<T> 接口,而是继承 Specification<T> 类。规约类已经的实现了 IsSatisfiedBy 方法。所以,我们只需要定义 ToExpression 方法。让我们来创建一些规约类:


//假设客户资产超过$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))
    {
    }
}

AndSpecificationSpecification 的子类,只有当规约的两边都匹配的时候才满足条件。那么,我么可以像其它规约一样来使用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对于报表不是十分关注;从性能的角度看,更应该优化查询来获取底层数据。