免責事項:すばらしい回答はまだないため、しばらく前に読んだすばらしいブログの投稿の一部を投稿することにしました。完全なブログ投稿はここにあります。だからここにあります:
次の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; }
}
このクラスは、User
オブジェクトの配列になる2つのパラメーターを持つクエリ操作を定義します。このメッセージを処理するクラスは、次のように定義できます。
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>
マッピングすることはできませんFindUsersBySearchTextQuery
しUser[]
。
注入IQueryHandler
しかし、消費者へのインタフェースを、まだ対処する必要があるいくつかのあまり目立たない問題があります。コンシューマーの依存関係の数は非常に大きくなり、コンストラクターの過剰注入につながる可能性があります-コンストラクターが引数を取りすぎる場合。クラスが実行するクエリの数は頻繁に変化する可能性があり、コンストラクター引数の数を常に変更する必要があります。
IQueryHandlers
抽象化のレイヤーを追加することで、注入しすぎる問題を解決できます。コンシューマーとクエリハンドラーの間に位置するメディエーターを作成します。
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
IQueryProcessor
1つの汎用の方法で非汎用インタフェースです。インターフェース定義でわかるように、はインターフェースにIQueryProcessor
依存していIQuery<TResult>
ます。これにより、に依存するコンシューマでのコンパイル時サポートが可能になりIQueryProcessor
ます。UserController
newを使用するようにを書き換えましょう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
すべてのクエリを処理できるに依存しています。メソッドを呼び出し、初期化クエリオブジェクトを渡す方法を。以来実装インタフェースを、私たちはジェネリックにそれを渡すことができます方法。C#の型推論により、コンパイラーはジェネリック型を判別できるため、型を明示的に指定する必要がなくなります。メソッドの戻り値の型もわかっています。UserController
SearchUsers
IQueryProcessor.Process
FindUsersBySearchTextQuery
IQuery<User[]>
Execute<TResult>(IQuery<TResult> query)
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動的キーワードを使用して)メソッドを呼び出す必要TQuery
があります。コンパイル時にジェネリック引数を使用できないため、この時点ではハンドラーインスタンスをキャストすることができないためです。ただし、Handle
メソッドの名前を変更したり、他の引数を取得したりしない限り、この呼び出しは失敗しません。必要に応じて、このクラスの単体テストを作成するのは非常に簡単です。リフレクションを使用するとわずかに低下しますが、心配する必要はありません。
あなたの懸念の1つに答えるには:
したがって、クエリ全体をカプセル化する代替手段を探していますが、それでも、スパゲッティリポジトリをコマンドクラスの急増に交換するだけではないほど十分に柔軟性があります。
この設計を使用すると、システムに多数の小さなクラスが存在することになりますが、(明確な名前を持つ)多数の小さなクラスに焦点を当てることは良いことです。このアプローチは、1つのクエリクラスにグループ化できるので、リポジトリ内の同じメソッドに異なるパラメーターを持つ多くのオーバーロードがある場合よりも明らかに優れています。したがって、リポジトリ内のメソッドよりもはるかに少ないクエリクラスを取得できます。