ABP框架系列之四十八:(Specifications-规范)
Introduction
Specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic (Wikipedia).
In pratical, it's mostly used to define reusable filters for entities or other business objects.
规范模式是一种特殊的软件设计模式,通过使用布尔逻辑将业务规则链接在一起,可以重新组合业务规则。
Example
In this section, we will see the need for specification pattern. This section is generic and not related to ABP's implementation.
在本节中,我们将看到规范模式的必要性。本节是通用的,与ABP的实现无关。
Assume that you have a service method that calculates total count of your customers as shown below:
public class CustomerManager { public int GetCustomerCount() { //TODO... return 0; } }
You probably will want to get customer count by a filter. For example, you may have premium customers (which have balance more than $100,000) or you may want to filter customers just by registration year. Then you can create other methods like GetPremiumCustomerCount(), GetCustomerCountRegisteredInYear(int year), GetPremiumCustomerCountRegisteredInYear(int year) and more. As you have more criteria, it's not possible to create a combination for every possibility.
您可能希望通过过滤器获得客户计数。例如,您可能有高级客户(余额超过100000美元),或者您可能希望通过注册年来过滤客户。然后,您可以创建其他方法如getpremiumcustomercount(),getcustomercountregisteredinyear(int year),getpremiumcustomercountregisteredinyear(int year)和更多的。由于有更多的标准,所以不可能为每种可能性创建一个组合。
One solution to this problem is the specification pattern. We could create a single method that gets a parameter as the filter:
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; } }
Thus, we can get any object as parameter that implements ISpecification<Customer> interface which is defined as shown below:
public interface ISpecification<T> { bool IsSatisfiedBy(T obj); }
And we can call IsSatisfiedBy with a customer to test if this customer is intented. Thus, we can use same GetCustomerCount with different filters without changing the method itself.
我们可以调用issatisfiedby用客户去测试这个客户是否希望。因此,我们可以使用相同的getcustomercount,传递不同滤波器,方法本身不改变。
While this solution is pretty fine in theory, it should be improved to better work in C#. For instance, it's not efficient to get all customers from database to check if they satisfy the given specification/condition. In the next section, we will see ABP's implementation which overcome this problem.
虽然这个方案在理论上是很好的,应该改进的更好的工作在C #。例如,从数据库中获取所有客户来检查它们是否满足给定的规格/条件是没有效率的。在下一节中,我们将看到ABP的实现来克服这个问题。
Creating Specification Classes
ABP defines the ISpecification interface as shown below:
public interface ISpecification<T> { bool IsSatisfiedBy(T obj); Expression<Func<T, bool>> ToExpression(); }
Adds a ToExpression() method which returns an expression and used to better integrate with IQueryable and Expression trees. Thus, we can easily pass a specification to a repository to apply a filter in the database level.
增加了一个toexpression()方法返回一个表达式,用于更好地整合IQueryable和表达式树。因此,我们可以轻松地将规范传递到存储库,以便在数据库级别应用筛选器。
We generally inherit from Specification<T> class instead of directly implementing ISpecification<T> interface. Specification class automatically implements IsSatisfiedBy method. So, we only need to define ToExpression. Let's create some specification classes:
我们一般从规范<T>类而不是直接实施ispecification <T>接口。规范类自动实现issatisfiedby方法。所以,我们只需要定义到。让我们创建一些规范类:
//Customers with $100,000+ balance are assumed as PREMIUM customers. public class PremiumCustomerSpecification : Specification<Customer> { public override Expression<Func<Customer, bool>> ToExpression() { return (customer) => (customer.Balance >= 100000); } } //A parametric specification example. 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); } }
As you see, we just implemented simple lambda expressions to define specifications. Let's use these specifications to get count of customers:
如您所见,我们只是实现了简单的lambda表达式来定义规范。让我们用这些规格来计算顾客的数量。
count = customerManager.GetCustomerCount(new PremiumCustomerSpecification()); count = customerManager.GetCustomerCount(new CustomerRegistrationYearSpecification(2017));
Using Specification With Repository(使用仓储规范)
Now, we can optimize CustomerManager to apply filter in the database:
现在,我们可以优化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()); } }
It's that simple. We can pass any specification to repositories since repositories can work with expressions as filters. In this example, CustomerManager is unnecessary since we could directly use repository with the specification to query database. But think that we want to execute a business operation on some customers. In that case, we could use specifications with a domain service to specify customers to work on.
就是这么简单。我们可以将任何规范传递给存储库,因为存储库可以使用表达式作为过滤器。在这个例子中,客户经理是不必要的因为我们可以直接使用库的规范来查询数据库。但是,我们想在某些客户上执行业务操作。在这种情况下,我们可以使用带有域服务的规范来指定要工作的客户。
Composing Specifications(排版规范)
One powerful feature of specifications is that they are composable with And, Or, Not and AndNot extension methods. Example:
规格的一个强大的特点是,他们是组合的,或者,不,而不是扩展方法。例子:
var count = customerManager.GetCustomerCount(new PremiumCustomerSpecification().And(new CustomerRegistrationYearSpecification(2017)));
We can even create a new specification class from existing specifications:
public class NewPremiumCustomersSpecification : AndSpecification<Customer> { public NewPremiumCustomersSpecification() : base(new PremiumCustomerSpecification(), new CustomerRegistrationYearSpecification(2017)) { } }
AndSpecification is a subclass of Specification class which satisfies only if both specifications are satisfied. Then we can use NewPremiumCustomersSpecification just like any other specification:
规范是规范类满足只有当满足一类规范。然后我们可以使用newpremiumcustomersspecification就像任何其他规范:
var count = customerManager.GetCustomerCount(new NewPremiumCustomersSpecification());
Discussion
While specification pattern is older than C# lambda expressions, it's generally compared to expressions. Some developers may think it's not needed anymore and we can directly pass expressions to a repository or to a domain service as shown below:
而规范的模式比C # lambda表达式古老,它通常与表达相比。一些开发人员可能认为不再需要它,我们可以直接将表达式传递到存储库或域服务,如下所示:
var count = _customerRepository.Count(c => c.Balance > 100000 && c.CreationYear == 2017);
Since ABP's Repository supports expessions, this is completely a valid usage. You don't have to define or use any specification in your application and you can go with expressions. So, what's the point of specification? Why and when should we consider to use them?
由于ABP的库支持的表达,这完全是一个有效的使用。您不必在应用程序中定义或使用任何规范,也可以使用表达式。因此,规范的重点是什么?为什么和什么时候我们应该考虑使用它们?
When To Use?
Some benefits of using specifications:
- Reusabe: Think that you need to PremiumCustomer filter in many places in your code base. If you go with expressions and not create a specification, what happens if you later change "Premium Customer" definition (say, you want to change minimum balance from $100,000 to $250,000 and add another condition like to be a customer older than 3). If you used specification, you just change a single class. If you used (copy/paste) same expression, you need to change all of them.
- Reusabe:认为你需要premiumcustomer过滤器在您的代码库,很多地方。如果您使用表达式而不是创建规范,如果您稍后更改“高级客户”定义(例如,您希望将最小余额从100000美元更改为250000美元,并添加另一个条件,如3岁以上的客户),会发生什么?。如果使用了规范,只需更改一个类。如果您使用(复制/粘贴)相同的表达式,则需要更改所有表达式。
- Composable: You can combine multiple specification to create new specifications. This is another type of reusability.
- 组合:可以将多个规范来创建新的规格。这是另一种类型的可重用性。
- Named: PremiumCustomerSpecification better explains the intent rather than a complex expression. So, if you have an expression that is meaningful in your business, consider to use specifications.
- 命名:premiumcustomerspecification更好的解释的意图,而不是一个复杂的表达式。因此,如果在业务中有一个有意义的表达式,请考虑使用规范。
- Testable: A specification is separately (and easily) testable object.
- 可测试性:规范是分开的(易于)可测试对象。
When To Not Use?
- Non business expressions: You can consider to not use specifications for non business related expressions and operations.
- 非业务表达式:您可以考虑不使用与非业务相关的表达式和操作的规范。
- Reporting: If you are just creating a report do not create specifications, but directly use IQueryable. Actually, you can even use plain SQL, Views or another tool for reporting. DDD does not care on reporting much and getting querying benefits of underlying data store can be important from performance point of view.
- 报告:如果你只是创建一个报告不创造规范,而是直接使用IQueryable。实际上,您甚至可以使用简单的SQL、视图或其他工具进行报告。DDD不关心报告太多,而从性能角度来看底层数据存储的查询好处是很重要的。