在一般意义上,规范对象只是包裹在对象中的谓词。如果一个谓词非常常用于一个类,那么将Move Method 谓词添加到它所适用的类中可能是有意义的。
当您构建像这样更复杂的东西时,这种模式会真正发挥作用:
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");
我一直在玩一些实验性代码,这些代码根据Expressions 实现规范,使用非常简单的静态构建器方法。
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;
}
}
}
也就是说,这是一整套没有增加价值的样板!这些Expressions 只查看公共属性,因此可以轻松地使用普通的旧 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 上调用时返回的稍微格式化的版本 - 请注意,如果您只有一个简单的委托,您根本无法看到表达式的结构!一些注意事项: DisplayClass 是一个编译器生成的类,它保存在闭包中捕获的局部变量,以处理 upwards funarg problem;而转储的 Expression 使用单个 = 符号来表示相等比较,而不是 C# 的典型 @ 987654341@.)
_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 并保留对在构建器方法中创建的闭包的引用。通过将闭包引用替换为其捕获的值和β-reducing 嵌套的 lambda(我还将 α-converted 所有参数名称替换为唯一生成的符号,作为简化 β-reduction 的中间步骤),一个更简单的Expression 树结果:
_0 => ((_0.AvailableFunds >= 500.00)
&& ((_0.AccountOpenDateTime <= (DateTime.UtcNow - 180.00:00:00))
&& ((_0.Address.State = "NY")
&& (_0.lastCreditScore >= 667))))
这些Expression 树可以进一步组合、编译成委托、漂亮打印、编辑、传递给理解Expression 树(例如EF 提供的树)的LINQ 接口,或者你有什么。
顺便说一句,我建立了一个愚蠢的小基准测试,实际上发现闭包引用消除对示例Expression 编译到委托时的评估速度有显着的性能影响 - 它缩短了评估时间几乎一半(!),在我碰巧坐在前面的机器上,每次调用从 134.1ns 到 70.5ns。另一方面,β-减少没有可察觉的差异,也许是因为编译无论如何都会这样做。在任何情况下,我都怀疑传统的规范类集能否在四个条件的组合下达到那种评估速度;如果出于其他原因(例如 builder-UI 代码的便利性)必须构建这样的常规类集,我认为最好让类集生成 Expression 而不是直接评估,但首先考虑您是否需要C# 中的模式 - 我见过太多规范过量的代码。