あなたがここで何をしようとしているのか、私は完全に理解しています。SOLID の原則を適用しているため、クエリの実装は、単にメッセージ (DTO) を送信して結果を取得する消費者から離れて抽象化されます。クエリの汎用インターフェースを実装することで、実装をデコレータでラップできます。これにより、トランザクション動作、監査、パフォーマンス監視、キャッシュなど、あらゆる種類の興味深い動作が可能になります。
これを行う方法は、メッセージ (クエリ定義) に対して次のインターフェイスを定義することです。
public interface IQuery<TResult> { }
そして、実装のために次のインターフェースを定義します。
public interface IQueryHandler<TQuery, TResult>
where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
これIQuery<TResult>
は一種のマーカー インターフェースですが、これにより、クエリが返すものを静的に定義できます。たとえば、次のようになります。
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
実装は次のように定義できます。
public class FindUsersBySearchTextQueryHandler
: IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
private readonly IUnitOfWork db;
public FindUsersBySearchTextQueryHandler(IUnitOfWork db)
{
this.db = db;
}
public User[] Handle(FindUsersBySearchTextQuery query)
{
// example
return (
from user in this.db.Users
where user.Name.Contains(query.SearchText)
where user.IsActive || query.IncludeInactiveUsers
select user)
.ToArray();
}
}
IQueryHandler<TQuery, TResult>
コンシューマーは、クエリを実行するために に依存することはできません。
public class UserController : Controller
{
IQueryHandler<FindUsersBySearchTextQuery, User[]> handler;
public UserController(
IQueryHandler<FindUsersBySearchTextQuery, User[]> handler)
{
this. handler = handler;
}
public View SearchUsers(string searchString)
{
var query = new FindUsersBySearchTextQuery
{
SearchText = searchString,
IncludeInactiveUsers = false
};
User[] users = this.handler.Handle(query);
return this.View(users);
}
}
これにより、消費者がこれを知ることなく、横断的な懸念事項をクエリ ハンドラーに追加できます。これにより、コンパイル時の完全なサポートが得られます。
ただし、このアプローチ (IMO) の最大の欠点は、(SRP に実際に違反することなく) 複数のクエリを実行する必要があることが多いため、簡単に大きなコンストラクターになってしまうことです。
IQueryHandler<TQuery, TResult>
これを解決するために、コンシューマーとインターフェースの間に抽象化を導入できます。
public interface IQueryProcessor
{
TResult Execute<TResult>(IQuery<TResult> query);
}
複数のIQueryHandler<TQuery, TResult>
実装を注入する代わりに、1 つを注入できます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
};
// Note how we omit the generic type argument,
// but still have type safety.
User[] users = this.queryProcessor.Execute(query);
return this.View(users);
}
}
実装は次のIQueryProcessor
ようになります。
sealed class QueryProcessor : IQueryProcessor
{
private readonly Container container;
public QueryProcessor(Container container)
{
this.container = container;
}
[DebuggerStepThrough]
public TResult Execute<TResult>(IQuery<TResult> query)
{
var handlerType = typeof(IQueryHandler<,>)
.MakeGenericType(query.GetType(), typeof(TResult));
dynamic handler = container.GetInstance(handlerType);
return handler.Handle((dynamic)query);
}
}
これはコンテナー (コンポジション ルートの一部) に依存し、dynamic
入力 (C# 4.0) を使用してクエリを実行します。
これIQueryProcessor
は実質的にあなたのQueryFactory
.
ただし、このIQueryProcessor
抽象化を使用することには欠点があります。たとえば、要求されたIQueryHandler<TQuery, TResult>
実装が存在するかどうかを DI コンテナーに検証させる可能性を見逃しています。processor.Execute
ルート オブジェクトを要求するときに代わりに呼び出すことがすぐにわかります。IQueryHandler<TQuery, TResult>
を実装する各クラスに が登録されているかどうかをチェックする追加の統合テストを作成することで、これを解決できますIQuery<TResult>
。もう 1 つの欠点は、依存関係があまり明確ではないことです (これIQueryProcessor
はある種のアンビエント コンテキストです)。これにより、単体テストが少し難しくなります。たとえば、消費者が新しいタイプのクエリを実行しても、単体テストはコンパイルされます。
この設計の詳細については、次のブログ投稿を参照してください: While… on the query side of my architecture .