我完全理解你在这里想要做什么。您正在应用 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);
}
Instread 注入多个IQueryHandler<TQuery, TResult>
实现,您可以注入一个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
不过,使用这种抽象也有缺点。例如,您错过了让您的 DI 容器验证请求的IQueryHandler<TQuery, TResult>
实现是否存在的可能性。您会processor.Execute
在请求根对象时发现您改为调用。您可以通过编写一个额外的集成测试来解决这个问题,该测试检查是否IQueryHandler<TQuery, TResult>
为每个实现IQuery<TResult>
. 另一个缺点是依赖关系不太清楚(这IQueryProcessor
是某种环境上下文),这使得单元测试更加困难。例如,当消费者运行一种新类型的查询时,您的单元测试仍将编译。
您可以在此博客文章中找到有关此设计的更多信息:同时……在我的架构的查询方面。