在一般意义上,规范对象只是包裹在对象中的谓词。如果谓词非常常与类一起使用,则将谓词移动到它适用的类中可能是有意义的。
当您构建像这样更复杂的东西时,这种模式会真正发挥作用:
var spec = new All(new CustomerHasFunds(500.00m),
new CustomerAccountAgeAtLeast(TimeSpan.FromDays(180)),
new CustomerLocatedInState("NY"));
并将其传递或序列化;当您提供某种“规范构建器”用户界面时,它会更有意义。
也就是说,C# 提供了更惯用的方式来表达这些事情,例如扩展方法和 LINQ:
var cutoffDate = DateTime.UtcNow - TimeSpan.FromDays(180); // captured
Expression<Func<Customer, bool>> filter =
cust => (cust.AvailableFunds >= 500.00m &&
cust.AccountOpenDateTime >= cutoffDate &&
cust.Address.State == "NY");
我一直在玩一些实验性的代码,这些代码Expression
用非常简单的静态构建器方法来实现规范。
public partial class Customer
{
public static partial class Specification
{
public static Expression<Func<Customer, bool>> HasFunds(decimal amount)
{
return c => c.AvailableFunds >= amount;
}
public static Expression<Func<Customer, bool>> AccountAgedAtLeast(TimeSpan age)
{
return c => c.AccountOpenDateTime <= DateTime.UtcNow - age;
}
public static Expression<Func<Customer, bool>> LocatedInState(string state)
{
return c => c.Address.State == state;
}
}
}
也就是说,这是一堆没有增加价值的样板! 这些Expression
s 只查看公共属性,因此可以轻松地使用普通的旧 lambda!现在,如果这些规范之一需要访问非公共状态,我们确实需要一个可以访问非公共状态的构建器方法。我将lastCreditScore
在这里用作示例。
public partial class Customer
{
private int lastCreditScore;
public static partial class Specification
{
public static Expression<Func<Customer, bool>> LastCreditScoreAtLeast(int score)
{
return c => c.lastCreditScore >= score;
}
}
}
我们还需要一种方法来组合这些规范 - 在这种情况下,需要所有子项都为真的组合:
public static partial class Specification
{
public static Expression<Func<T, bool>> All<T>(params Expression<Func<T, bool>>[] tail)
{
if (tail == null || tail.Length == 0) return _0 => true;
var param = Expression.Parameter(typeof(T), "_0");
var body = tail.Reverse()
.Skip(1)
.Aggregate((Expression)Expression.Invoke(tail.Last(), param),
(current, item) =>
Expression.AndAlso(Expression.Invoke(item, param),
current));
return Expression.Lambda<Func<T, bool>>(body, param);
}
}
我想这样做的部分缺点是它可能导致复杂的Expression
树。例如,构建这个:
var spec = Specification.All(Customer.Specification.HasFunds(500.00m),
Customer.Specification.AccountAgedAtLeast(TimeSpan.FromDays(180)),
Customer.Specification.LocatedInState("NY"),
Customer.Specification.LastCreditScoreAtLeast(667));
生成Expression
一棵看起来像这样的树。(这些是ToString()
在调用时返回的稍微格式化的版本Expression
- 请注意,如果您只有一个简单的委托,您根本无法看到表达式的结构!一些注意事项:aDisplayClass
是编译器生成的保存在闭包中捕获的局部变量的类,以处理向上的 funarg 问题;并且转储Expression
使用单个=
符号来表示相等比较,而不是 C# 的典型==
。)
_0 => (Invoke(c => (c.AvailableFunds >= value(ExpressionExperiment.Customer+Specification+<>c__DisplayClass0).amount),_0)
&& (Invoke(c => (c.AccountOpenDateTime <= (DateTime.UtcNow - value(ExpressionExperiment.Customer+Specification+<>c__DisplayClass2).age)),_0)
&& (Invoke(c => (c.Address.State = value(ExpressionExperiment.Customer+Specification+<>c__DisplayClass4).state),_0)
&& Invoke(c => (c.lastCreditScore >= value(ExpressionExperiment.Customer+Specification+<>c__DisplayClass6).score),_0))))
凌乱!大量调用即时 lambda 并保留对在构建器方法中创建的闭包的引用。通过用它们捕获的值替换闭包引用并对嵌套的 lambdas 进行 β 归约(我还将所有参数名称转换为唯一生成的符号,作为简化 β 归约的中间步骤),得到一个更简单的Expression
树:
_0 => ((_0.AvailableFunds >= 500.00)
&& ((_0.AccountOpenDateTime <= (DateTime.UtcNow - 180.00:00:00))
&& ((_0.Address.State = "NY")
&& (_0.lastCreditScore >= 667))))
Expression
然后可以进一步组合这些树,编译成委托,漂亮地打印,编辑,传递给理解Expression
树的 LINQ 接口(例如 EF 提供的那些),或者你有什么。
在旁注中,我建立了一个愚蠢的小微基准,实际上发现闭包引用消除对Expression
编译给委托的示例的评估速度有显着的性能影响——它将评估时间减少了近一半(!) ,在我碰巧坐在前面的机器上,每次调用从 134.1ns 到 70.5ns。另一方面,β-减少没有可察觉的差异,也许是因为编译无论如何都会这样做。在任何情况下,我都怀疑传统的规范类集能否在四个条件的组合下达到那种评估速度;如果出于其他原因(例如 builder-UI 代码的便利性)必须构建这样的常规类集,我认为让类集生成一个Expression
而不是直接评估,而是首先考虑您是否需要 C# 中的模式 - 我已经看到太多规范过度使用的代码。