仕様パターンは無意味ですか?
-
10-10-2019 - |
質問
次の例を考えると、仕様パターンが無意味であるかどうか疑問に思っています。
顧客がアカウントに十分なバランスがあるかどうかを確認すると、次のような仕様を作成するとします。
new CustomerHasEnoughMoneyInTheirAccount().IsSatisfiedBy(customer)
しかし、私が疑問に思っているのは、このような顧客クラスでプロパティゲッターを使用することにより、仕様パターンの同じ「利点」を達成できることです。
public class Customer
{
public double Balance;
public bool HasEnoughMoney
{
get { return this.Balance > 0; }
}
}
クライアントコードから:
customer.HasEnoughMoney
だから私の質問は本当にです。プロパティゲッターを使用してビジネスロジックをラップすることと、仕様クラスの作成との違いは何ですか?
よろしくお願いします!
解決
仕様クラスを使用すると、オブジェクト自体を変更せずに新しい基準を作成できるためです。
他のヒント
一般的な意味では、仕様オブジェクトは、オブジェクトに包まれた述語にすぎません。述語がクラスで非常に一般的に使用されている場合、それは理にかなっているかもしれません メソッドを移動します それが適用されるクラスへの述語。
このようなより複雑なものを構築するとき、このパターンは本当に独自のものになります:
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
s公共のプロパティのみを見るので、単純な古いラムダを簡単に使用できます!さて、これらの仕様の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));
Anを生成します Expression
このように見える木。 (これらは、何がわずかにフォーマットされたバージョンです ToString()
呼び出されたときに戻ります Expression
- 単純な代表者しかいなければ、表現の構造をまったく見ることができないことに注意してください!いくつかのメモ:a DisplayClass
閉鎖中にキャプチャされたローカル変数を保持するコンパイラ生成クラスで、 上向きの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))))
混雑!即時のラムダの呼び出しと、ビルダーの方法で作成された閉鎖への参照を保持しました。閉鎖参照をキャプチャされた値に置き換えることによって β削減 ネストされたラムダス(私も α変換 β還元を簡素化するための中間ステップとしての一意の生成された記号に対するすべてのパラメーター名、はるかにシンプルです Expression
ツリーの結果:
_0 => ((_0.AvailableFunds >= 500.00)
&& ((_0.AccountOpenDateTime <= (DateTime.UtcNow - 180.00:00:00))
&& ((_0.Address.State = "NY")
&& (_0.lastCreditScore >= 667))))
これらは Expression
その後、ツリーをさらに結合し、デリゲートに編集し、かなり印刷され、編集され、理解しているLinqインターフェイスに渡されます Expression
木(EFが提供するものなど)、またはあなたは何を持っていますか。
サイドノートでは、私は愚かな小さなマイクロベンチマークを構築し、実際に閉鎖参照排除が例の評価速度に顕著なパフォーマンスに影響を与えることを発見しました Expression
デリゲートにコンパイルされた場合、評価時間をほぼ半分(!)に削減し、マシンのコールごとに134.1NSから70.5nsから70.5nsまで削減します。一方、β還元は、おそらくコンピレーションがそれを行うため、検出可能な違いを生みませんでした。いずれにせよ、私は従来の仕様クラスセットが、4つの条件の複合材のそのような評価速度に達する可能性があるとは思わない。このような従来のクラスセットを、Builder-UIコードの利便性など、他の理由で構築する必要がある場合、クラスセットを生成することをお勧めします。 Expression
直接評価するのではなく、最初にC#でパターンが必要かどうかを検討します。仕様を超えるコードが多すぎることがわかりました。
はい、それは無意味です。
ウィキペディアの記事 このパターンを長々と批判します。しかし、私は最大の批判は単にあると思います 内部プラットフォーム効果. 。なぜオペレーターを再発明するのですか?繰り返しになりますが、ウィキペディアの記事を読んでください。
ヘンリー、あなたは財産が優れていると仮定するのは正しいです。なぜ、よりシンプルで理解された概念を避け、その概念ではあなたの質問に答えないという不明瞭な「パターン」のためになぜですか?それはアイデアですが、悪いものです。それはアンチパターンであり、あなたに対して機能するパターン、コーダーです。
違いは何ですか?しかし、より便利な質問は、仕様パターンをいつ使用すべきかということです。
このパターンを使用しないでください, 、このパターンの私の一般的なルールです。
まず、これは一般的な理論ではなく、特定のパターンにすぎないことを認識する必要があります。クラスの特定のモデリング{仕様、および仕様、...}。より広範なドメイン駆動型理論を念頭に置いて、このパターンを放棄しても、誰もがよく知っている優れたオプションを持つことができます。たとえば、ドメイン言語とロジックをモデル化するために、名前が付けられたオブジェクト/メソッド/プロパティです。
ジェフリーは言った:
仕様オブジェクトは、オブジェクトに包まれた述語にすぎません
これはドメイン駆動型に当てはまりますが、仕様パターンではありません。 Jeffreyは、IQUERYABLE式を動的に構築したい状況を包括的に説明しているため、データストア(SQLデータベース)で効率的に実行できます。彼の最後の結論は、仕様パターンが規定されているようにそれを行うことはできないということです。 JeffreyのiQueryable Expression Treesは、論理ルールを分離し、異なる複合材料に適用するための1つの代替方法です。彼の例コードからわかるように、それは冗長で、一緒に仕事をするのは非常に厄介です。このような動的な複合材料を必要とする状況も想像できません。必要に応じて、よりシンプルな他の多くの手法が利用可能です。
私たちは皆、あなたが最後にパフォーマンスを最適化する必要があることを知っています。ここで達成しようとしています 出血エッジ iQueryable Expression Treesでは、trapです。代わりに、最初にシンプルで簡潔なプロパティゲッターである最高のツールから始めます。次に、どの作業が残っているかをテストし、評価し、優先順位を付けます。
この仕様パターンが必要/より良い状況をまだ経験していません。想定される状況に出くわすと、ここにリストして反論します。良い状況に出くわした場合は、この回答を新しいセクションで修正します。
Re:Zerkmsの答え
仕様クラスを使用すると、オブジェクト自体を変更せずに新しい基準を作成できます。
C#はすでにそのような状況に対応しています:
- 継承(一般的に)、その後、継承クラスを拡張します(これは、クラスが来る場所から名前空間/ライブラリを所有していない場合に適しています)
- 継承をオーバーライドする方法
- 部分的 - データモデルクラスがある場合に最適です。 [NotStored]プロパティを一緒に追加し、オブジェクトから直接必要な情報にアクセスするすべての至福を楽しむことができます。あなたが押すとき。 Intellisenseは、どのメンバーが利用できるかを教えてくれます。
- 継承が実用的でない場合(アーキテクチャはサポートしていない)、または親クラスが密封されている場合、拡張方法は優れています。
私が引き継ぐプロジェクトでは、仕様パターンのようなアンチパターンなどに遭遇します。彼らはしばしば別のプロジェクト/ライブラリにいます(プロジェクトの過剰なフラグメントは別のひどい習慣です)。誰もがオブジェクトを拡張するにはあまりにも怖いです。
Zerkmsの回答を参照してください。Plus:仕様は、インターフェイスなどの抽象タイプでも機能したり、ジェネリックとして機能したりすることもできます。
または、顧客に対して行う必要があるチェックは、コンテキストに依存する場合があります。たとえば、顧客オブジェクトはまだ有料ロールシステムでは有効ではありませんが、ユーザーが再びログインしたときにさらに処理するプロセスの途中でデータベースに保存するのに有効です。仕様を使用すると、関連するチェックのグループを集中型の場所に構築し、コンテキストに応じてセット全体を切り替えることができます。この状況では、たとえば工場パターンと組み合わせることになります。