Aviso: Como ainda não há boas respostas, decidi postar uma parte de um ótimo post de blog que li há um tempo, copiado quase literalmente. Você pode encontrar a postagem completa do blog aqui . Então aqui está:
Podemos definir as duas interfaces a seguir:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
O IQuery<TResult>especifica uma mensagem que define uma consulta específica com os dados que retorna usando o TResulttipo genérico. Com a interface definida anteriormente, podemos definir uma mensagem de consulta como esta:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
Esta classe define uma operação de consulta com dois parâmetros, que resultará em uma matriz de Userobjetos. A classe que lida com essa mensagem pode ser definida da seguinte maneira:
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();
}
}
Agora podemos permitir que os consumidores dependam da IQueryHandlerinterface genérica :
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);
}
}
Imediatamente, esse modelo nos dá muita flexibilidade, porque agora podemos decidir o que injetar no UserController. Podemos injetar uma implementação completamente diferente, ou que envolva a implementação real, sem ter que fazer alterações no UserController(e em todos os outros consumidores dessa interface).
A IQuery<TResult>interface nos dá suporte em tempo de compilação ao especificar ou injetar IQueryHandlersem nosso código. Quando alteramos o FindUsersBySearchTextQuerypara return em UserInfo[]vez (por implementação IQuery<UserInfo[]>), o UserControllerfalhará ao compilar, já que a restrição de tipo genérico em IQueryHandler<TQuery, TResult>não será capaz de mapear FindUsersBySearchTextQuerypara User[].
IQueryHandlerNo entanto, injetar a interface em um consumidor apresenta alguns problemas menos óbvios que ainda precisam ser resolvidos. O número de dependências de nossos consumidores pode ficar muito grande e pode levar à injeção excessiva do construtor - quando um construtor recebe muitos argumentos. O número de consultas que uma classe executa pode mudar com frequência, o que exigiria mudanças constantes no número de argumentos do construtor.
Podemos resolver o problema de ter que injetar muitos IQueryHandlerscom uma camada extra de abstração. Criamos um mediador que fica entre os consumidores e os manipuladores de consulta:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
O IQueryProcessoré uma interface não genérica com um método genérico. Como você pode ver na definição da interface, o IQueryProcessordepende da IQuery<TResult>interface. Isso nos permite ter suporte de tempo de compilação em nossos consumidores que dependem do IQueryProcessor. Vamos reescrever o UserControllerpara usar o novo 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);
}
}
O UserControlleragora depende de um IQueryProcessorque pode lidar com todas as nossas consultas. O UserController's SearchUsersmétodo chama o IQueryProcessor.Processmétodo passando um objeto de consulta inicializado. Como o FindUsersBySearchTextQueryimplementa a IQuery<User[]>interface, podemos passá-lo para o Execute<TResult>(IQuery<TResult> query)método genérico . Graças à inferência de tipo C #, o compilador é capaz de determinar o tipo genérico e isso nos economiza a necessidade de declarar explicitamente o tipo. O tipo de retorno do Processmétodo também é conhecido.
Agora é responsabilidade da implementação do IQueryProcessorencontrar o que é certo IQueryHandler. Isso requer alguma digitação dinâmica e, opcionalmente, o uso de uma estrutura de injeção de dependência, e tudo pode ser feito com apenas algumas linhas de código:
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);
}
}
A QueryProcessorclasse constrói um IQueryHandler<TQuery, TResult>tipo específico com base no tipo da instância de consulta fornecida. Este tipo é usado para solicitar à classe de contêiner fornecida para obter uma instância desse tipo. Infelizmente, precisamos chamar o Handlemétodo usando reflexão (usando a palavra-chave dymamic do C # 4.0 neste caso), porque neste ponto é impossível converter a instância do manipulador, uma vez que o TQueryargumento genérico não está disponível no momento da compilação. No entanto, a menos que o Handlemétodo seja renomeado ou obtenha outros argumentos, esta chamada nunca falhará e, se você quiser, é muito fácil escrever um teste de unidade para esta classe. Usar reflexão causará uma ligeira queda, mas não é nada para se preocupar.
Para responder a uma de suas preocupações:
Portanto, estou procurando alternativas que encapsulem toda a consulta, mas ainda flexíveis o suficiente para que você não esteja apenas trocando Repositórios de espaguete por uma explosão de classes de comando.
Uma consequência do uso desse design é que haverá muitas classes pequenas no sistema, mas ter muitas classes pequenas / focadas (com nomes claros) é uma coisa boa. Esta abordagem é claramente muito melhor do que ter muitas sobrecargas com parâmetros diferentes para o mesmo método em um repositório, já que você pode agrupá-los em uma classe de consulta. Portanto, você ainda obtém muito menos classes de consulta do que métodos em um repositório.