一般的な意味では、仕様オブジェクトは、オブジェクトにラップされた単なる述語です。述語がクラスで非常に一般的に使用されている場合は、メソッドを適用するクラスに述語を移動することが理にかなっている場合があります。
このパターンは、次のようなより複雑なものを構築しているときに実際に発生します。
var spec = new All(new CustomerHasFunds(500.00m),
new CustomerAccountAgeAtLeast(TimeSpan.FromDays(180)),
new CustomerLocatedInState("NY"));
そしてそれを回すかシリアル化する。ある種の「仕様ビルダー」UIを提供する場合は、さらに意味があります。
とは言うものの、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
、非常に単純な静的ビルダーメソッドを使用して、sの観点から仕様を実装するいくつかの実験的なコードで遊んでいます。
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
はパブリックプロパティのみを参照するため、単純な古いラムダを同じように簡単に使用できます。さて、これらの仕様の1つが非公開状態にアクセスする必要がある場合、非公開状態にアクセスできるビルダーメソッドが本当に必要です。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問題を処理するために、クロージャでキャプチャされたローカル変数を保持するクラス。ダンプされたものは、C#の典型的なものではなくExpression
、単一の符号を使用して等価比較を表します。)=
==
_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))))
混雑!即時ラムダの多くの呼び出しと、ビルダーメソッドで作成されたクロージャーへの保持された参照。クロージャ参照をキャプチャされた値に置き換え、ネストされたラムダをβ-リダクションすることで(β-リダクションを単純化するための中間ステップとして、すべてのパラメーター名を一意の生成されたシンボルにα-変換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まで。一方、β還元は、おそらくコンパイルがとにかくそれを行うため、検出可能な違いをもたらさなかった。いずれにせよ、従来の仕様クラスセットが4つの条件の複合でそのような評価速度に達することができるとは思えません。ビルダーUIコードの利便性など、他の理由でこのような従来のクラスセットを構築する必要がある場合は、クラスセットにExpression
直接評価するのではなく、最初にC#でパターンが必要かどうかを検討します。仕様が過剰に使用されているコードを見てきました。