14

次の例を考えると、仕様パターンが無意味であるかどうか疑問に思っています。

顧客のアカウントに十分な残高があるかどうかを確認したい場合は、次のような仕様を作成します。

new CustomerHasEnoughMoneyInTheirAccount().IsSatisfiedBy(customer)

ただし、私が疑問に思っているのは、次のようにCustomerクラスのProperty getterを使用することで、仕様パターンと同じ「利点」(ビジネスルールを適切に変更するだけでよいなど)を実現できることです。

public class Customer
{

     public double Balance;

     public bool HasEnoughMoney
     {
          get { return this.Balance > 0; }
     }
}

クライアントコードから:

customer.HasEnoughMoney

だから私の質問は本当にです。プロパティゲッターを使用してビジネスロジックをラップすることと、仕様クラスを作成することの違いは何ですか?

よろしくお願いします!

4

4 に答える 4

11

はい、それは無意味です。

ウィキペディアの記事は、このパターンを詳細に批判しています。しかし、最大の批判は、内部プラットフォーム効果のみであると思います。AND演算子を再発明するのはなぜですか?全体像については、ウィキペディアの記事を必ずお読みください。

ヘンリー、あなたはプロパティゲットが優れていると仮定するのは正しいです。なぜ、より単純でよく理解されているOOの概念を避けて、その概念ではあなたの質問に答えられないあいまいな「パターン」を避けるのでしょうか。それはアイデアですが、悪いアイデアです。これはアンチパターンであり、コーダーであるあなたに逆らうパターンです。

違いは何ですかと質問しましたが、より有用な質問は、仕様パターンをいつ使用する必要があるかということです。

このパターンは絶対に使用しないでください。これがこのパターンの私の一般的なルールです。

まず、このパターンは科学理論に基づいているのではなく、クラスの特定のモデリングを使用する誰かが想像した任意のパターンにすぎないことを理解する必要があります{仕様、AndSpecification、...}。より広範なドメイン駆動理論を念頭に置いて、このパターンを放棄しても、誰もが精通している優れたオプションを利用できます。たとえば、ドメイン言語とロジックをモデル化するための名前の付いたオブジェクト/メソッド/プロパティなどです。

ジェフリーは言った:

仕様オブジェクトは、オブジェクトにラップされた単なる述語です。

これはドメイン駆動型には当てはまりますが、特に仕様パターンには当てはまりません。Jeffreyは、IQueryable式を動的に構築して、データストア(SQLデータベース)で効率的に実行できるようにする状況を包括的に説明しています。彼の最後の結論は、規定されている仕様パターンではそれを行うことができないということです。JeffreyのIQueryable式ツリーは、論理ルールを分離し、それらをさまざまなコンポジットに適用するための1つの代替方法です。彼のサンプルコードからわかるように、それは冗長で、操作するのが非常に厄介です。そのようなダイナミックなコンポジットを必要とするような状況も想像できません。そして、必要に応じて、より簡単な他の多くのテクニックが利用可能です:-

最後にパフォーマンスを最適化する必要があることは誰もが知っています。ここでIQueryable式ツリーを使用してブリーディングエッジを実現しようとするのは、罠です。代わりに、最初にシンプルで簡潔なプロパティゲッターという最高のツールから始めてください。次に、残っている作業をテスト、評価、優先順位付けします。

この仕様パターンが必要/より良い状況をまだ経験していません。想定される状況に出くわしたので、ここにリストして反論します。良い状況に出くわした場合は、この回答を新しいセクションで修正します。

RE:zerkmsの答え

仕様クラスを使用すると、オブジェクト自体を変更せずに新しい基準[原文のまま]を作成できるためです。

C#はすでにそのような状況に対応しています:

  • 継承(一般的に)、継承されたクラスを拡張します(これは、クラスが発生した場所から名前空間/ライブラリを所有していない場合に適しています)
  • 継承でオーバーライドするメソッド
  • 部分的-データモデルクラスがある場合に最適です。[NotStored]プロパティを一緒に追加して、オブジェクトから直接必要な情報にアクセスすることのすべての至福を楽しむことができます。'。'を押すと IntelliSenseは、使用可能なメンバーを通知します。
  • 拡張メソッドは、継承が実用的でない場合(アーキテクチャが継承をサポートしていない場合)、または親クラスが封印されている場合に最適です。

そして、これらはグローバルに教えられたアイデアであり、ほとんどのプログラマーはすでに自然に理解して使用しています。

私が引き継ぐプロジェクトでは、仕様パターンなどのアンチパターンに遭遇します。それらはしばしば別のプロジェクト/ライブラリにあり(プロジェクトの過度の断片化は別のひどい習慣です)、誰もがオブジェクトを拡張するにはあまりにも怖いです。

于 2016-09-30T07:06:01.493 に答える
10

仕様クラスを使用すると、オブジェクト自体を変更せずに新しい基準を作成できるためです。

于 2010-12-15T02:33:02.653 に答える
10

一般的な意味では、仕様オブジェクトは、オブジェクトにラップされた単なる述語です。述語がクラスで非常に一般的に使用されている場合は、メソッドを適用するクラスに述語を移動することが理にかなっている場合があります。

このパターンは、次のようなより複雑なものを構築しているときに実際に発生します。

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#でパターンが必要かどうかを検討します。仕様が過剰に使用されているコードを見てきました。

于 2010-12-15T02:42:22.790 に答える
3

zerkmsの回答に加えて、仕様を参照してください。仕様は、インターフェイスなどの抽象型で機能することも、オブジェクトの全範囲に適用できるようにする汎用として機能することもできます。

または、顧客に対して実行する必要のあるチェックは、コンテキストによって異なる場合があります。たとえば、顧客オブジェクトは、有料ロールシステムではまだ有効ではないかもしれませんが、ユーザーが再度ログインしたときにさらに処理するために、プロセスの途中でデータベースに保存するために有効です。仕様を使用すると、関連するチェックのグループを一元化された場所に構築し、コンテキストに応じてセット全体を切り替えることができます。この状況では、たとえばファクトリパターンと組み合わせます。

于 2010-12-15T02:44:52.790 に答える