免責事項:まだ素晴らしい答えがないので、私はしばらく前に読んだ素晴らしいブログ投稿の一部をほぼ逐語的にコピーして投稿することにしました。あなたはここで完全なブログ投稿を見つけることができます。だからここにあります:
次の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>
FindUsersBySearchTextQuery
User[]
ただし、インターフェイスをコンシューマーに注入する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
ます。はインターフェースを実装しているので、それを汎用メソッドに渡すことができます。C#型推論のおかげで、コンパイラーはジェネリック型を判別できるため、型を明示的に指定する必要がなくなります。メソッドの戻りタイプも知られています。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 dymamicキーワードを使用して)メソッドを呼び出す必要があります。これTQuery
は、コンパイル時に汎用引数が使用できないため、現時点ではハンドラーインスタンスをキャストできないためです。ただし、Handle
メソッドの名前が変更されるか、他の引数を取得しない限り、この呼び出しは失敗することはなく、必要に応じて、このクラスの単体テストを作成するのは非常に簡単です。リフレクションを使用するとわずかに低下しますが、実際に心配する必要はありません。
あなたの懸念の1つに答えるために:
だから私はクエリ全体をカプセル化する代替案を探していますが、それでも十分な柔軟性があるので、コマンドクラスの爆発的な増加のためにスパゲッティリポジトリを交換するだけではありません。
この設計を使用した結果、システムには小さなクラスがたくさんありますが、(明確な名前を持つ)小さな/焦点を絞ったクラスがたくさんあるのは良いことです。このアプローチは、リポジトリ内の同じメソッドに対して異なるパラメーターを持つ多くのオーバーロードを使用するよりも明らかにはるかに優れています。これは、それらを1つのクエリクラスにグループ化できるためです。したがって、リポジトリ内のメソッドよりもはるかに少ないクエリクラスを取得できます。