1. ホーム
  2. c#

[解決済み] よく設計されたクエリーコマンドと仕様

2023-03-20 02:24:23

質問

典型的なリポジトリパターンが示す問題(特殊なクエリのためのメソッドの増加など)に対する良い解決策をかなり長い間探しています。 http://ayende.com/blog/3955/repository-is-the-new-singleton ).

私は、Command クエリ、特に Specification パターンを使用したクエリのアイデアがとても好きです。 しかし、仕様に関する私の問題は、それが単純な選択の基準 (基本的に、where 節) にのみ関係し、結合、グループ化、サブセットの選択または投影など、クエリの他の問題を扱わないということです。

(注意: 私は Command パターンのように、quot;command" という用語を使用します(query オブジェクトとしても知られています)。 コマンド/クエリ分離のように、クエリとコマンド (更新、削除、挿入) を区別して話しているのではありません。)

ですから、私はクエリ全体をカプセル化し、かつ、コマンドクラスの爆発のためにスパゲッティリポジトリを交換するだけではない、十分に柔軟な代替品を探しているのです。

私は、たとえば Linqspecs を使用しましたが、選択基準に意味のある名前を割り当てることができることにいくらかの価値を見いだしましたが、それだけでは十分ではありません。 おそらく私は、複数のアプローチを組み合わせた混合ソリューションを求めているのだと思います。

私は、この問題に対処するため、または別の問題に対処するがこれらの要件は満たすために、他の人が開発した可能性のあるソリューションを探しています。 リンクされた記事で、Ayende は nHibernate コンテキストを直接使用することを提案していますが、ビジネス層がクエリ情報を含む必要があるため、大きく複雑になっていると感じます。

私は、待機期間が過ぎたらすぐに、この件に関して報奨金を提供するつもりです。 ですから、あなたのソリューションを賞金に値するものにしてください、良い説明とともに、私は最も良いソリューションを選択し、次点者をアップヴォートします。

注:私はORMベースのものを探しています。 EF または nHibernate である必要はありませんが、これらは最も一般的であり、最も適していると思われます。 もし、他のORMに簡単に適応できるのであれば、それはボーナスになります。 また、Linq互換性があるとよいでしょう。

UPDATE: ここにあまり良い提案がないことに本当に驚いています。 人々は完全に CQRS か、完全にリポジトリ派であるかのどちらかのように見えます。 私のアプリのほとんどは、CQRS を保証するほど複雑ではありません (ほとんどの CQRS 支持者は、CQRS を使用すべきではないと簡単に言いますが)。

UPDATE: ここで少し混乱があるようです。 私は新しいデータ アクセス テクノロジーを探しているのではなく、ビジネスとデータの間の合理的によく設計されたインターフェイスを探しているのです。

理想的には、Queryオブジェクト、Specificationパターン、リポジトリの間のある種の交差を求める。 上で述べたように、Specificationパターンはwhere句の側面だけを扱い、joinやsub-selectなど、クエリの他の側面は扱わない。 リポジトリはクエリ全体を扱いますが、しばらくすると手に負えなくなります。 クエリオブジェクトもクエリ全体を扱いますが、クエリオブジェクトの爆発でリポジトリを単純に置き換えることはしたくありません。

どのように解決するのか?

免責事項です。 まだ素晴らしい回答がないので、私がしばらく前に読んだ素晴らしいブログ記事の一部を、ほぼそのままコピーして掲載することにしました。ブログの記事全文はこちらでご覧いただけます。 はこちら . というわけで、こちらです。


以下の2つのインターフェイスを定義することができます。

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

IQuery<TResult> を使って特定のクエリを定義するメッセージを指定し、それが返すデータを TResult ジェネリックタイプを使用します。先に定義したインターフェースで、次のようなクエリメッセージを定義することができます。

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }
    public bool IncludeInactiveUsers { get; set; }
}

このクラスは、2つのパラメータを持つクエリ操作を定義し、その結果、配列は User オブジェクトを生成します。このメッセージを処理するクラスは,次のように定義できます.

public class FindUsersBySearchTextQueryHandler
    : IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
    private readonly NorthwindUnitOfWork db;

    public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
    {
        this.db = db;
    }

    public User[] Handle(FindUsersBySearchTextQuery query)
    {
        return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
    }
}

これで、消費者が汎用的な IQueryHandler インターフェースに依存させることができます。

public class UserController : Controller
{
    IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;

    public UserController(
        IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
    {
        this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        User[] users = this.findUsersBySearchTextHandler.Handle(query);    
        return View(users);
    }
}

すぐにこのモデルは多くの柔軟性を与えてくれます。 UserController . を変更することなく、全く別の実装を注入したり、実際の実装をラップしたものを注入したりすることができます。 UserController (そしてそのインターフェースの他の全ての消費者) に変更を加えることなく、完全に異なる実装、または本当の実装をラップしたものを注入することができます。

IQuery<TResult> を指定したり注入したりする際のコンパイル時のサポートを提供します。 IQueryHandlers を指定する際のコンパイル時のサポートを提供します。を変更すると FindUsersBySearchTextQuery を返すように変更すると UserInfo[] を返すのではなく、(実装することで IQuery<UserInfo[]> を実装することで、) UserController の総称型制約があるため、コンパイルに失敗します。 IQueryHandler<TQuery, TResult> をマップすることができないからです。 FindUsersBySearchTextQueryUser[] .

を注入することで IQueryHandler インターフェイスをコンシューマに注入することは、しかし、まだ対処する必要のある、あまり目立たない問題があります。コンシューマの依存関係の数が大きくなりすぎて、コンストラクタのオーバーインジェクション (コンストラクタがあまりにも多くの引数を取ること) につながる可能性があります。クラスが実行するクエリの数は頻繁に変わる可能性があり、コンストラクタの引数の数を常に変更する必要があります。

を注入しなければならないという問題を修正することができます。 IQueryHandlers を注入しなければならないという問題を解決することができます。コンシューマとクエリハンドラの間に位置するメディエータを作成します。

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

IQueryProcessor は1つの汎用メソッドを持つ非汎用インターフェイスです。インターフェイスの定義でわかるように IQueryProcessor に依存しています。 IQuery<TResult> インターフェースに依存します。このため、コンパイル時に IQueryProcessor . を書き換えてみましょう。 UserController を書き換えて、新しい IQueryProcessor :

public class UserController : Controller
{
    private IQueryProcessor queryProcessor;

    public UserController(IQueryProcessor queryProcessor)
    {
        this.queryProcessor = queryProcessor;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        // Note how we omit the generic type argument,
        // but still have type safety.
        User[] users = this.queryProcessor.Process(query);

        return this.View(users);
    }
}

UserController に依存するようになりました。 IQueryProcessor に依存しています。この UserController 's SearchUsers メソッドは IQueryProcessor.Process メソッドを呼び出し、初期化されたクエリーオブジェクトを渡します。メソッドは FindUsersBySearchTextQuery を実装しているので IQuery<User[]> インターフェイスを実装しているので、それを一般的な Execute<TResult>(IQuery<TResult> query) メソッドに渡すことができます。C#の型推論のおかげで、コンパイラはジェネリックの型を決定することができ、これによって型を明示的に記述する必要がなくなります。メソッドの戻り値の型は Process メソッドの戻り値の型もわかっています。

の実装の責任になりました。 IQueryProcessor を見つけるのは IQueryHandler . これには動的型付けと、オプションで依存性注入フレームワークを使用する必要がありますが、すべて数行のコードで行うことができます。

sealed class QueryProcessor : IQueryProcessor
{
    private readonly Container container;

    public QueryProcessor(Container container)
    {
        this.container = container;
    }

    [DebuggerStepThrough]
    public TResult Process<TResult>(IQuery<TResult> query)
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(query.GetType(), typeof(TResult));

        dynamic handler = container.GetInstance(handlerType);

        return handler.Handle((dynamic)query);
    }
}

QueryProcessor クラスは、特定の IQueryHandler<TQuery, TResult> を構築します。この型は、提供されたコンテナクラスにその型のインスタンスを取得するよう依頼するために使用されます。残念ながら、私たちは Handle メソッドをリフレクションを使って呼び出す必要があります (この場合、C# 4.0 dymamic キーワードを使用します)。 TQuery 引数がコンパイル時に利用できないため、この時点でハンドラインスタンスをキャストすることができないからです。しかし Handle メソッドの名前が変更されるか、他の引数を取得しない限り、この呼び出しは決して失敗しませんし、もし望むなら、このクラスのためのユニットテストを書くのはとても簡単です。リフレクションを使用すると、若干の低下が生じますが、本当に心配することは何もありません。


あなたの懸念の 1 つに答えます。

ですから、私はクエリ全体をカプセル化する代替手段を探しています。 しかし、スパゲッティを交換するだけでなく、十分な柔軟性があります。 リポジトリとコマンドクラスの交換にならない程度の柔軟性があります。

この設計を使用することの結果として、システム内に多くの小さなクラスが存在することになりますが、多くの小さな/フォーカスされたクラス(明確な名前を持つ)を持つことは良いことです。この方法は、リポジトリにある同じメソッドに対して異なるパラメータを持つ多くのオーバーロードを持つよりも明らかに優れており、それらを1つのクエリクラスにまとめることができます。つまり、リポジトリにあるメソッドよりも、クエリクラスの方がずっと少ないのです。