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 TResult
tipo 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 User
objetos. 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 IQueryHandler
interface 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 IQueryHandlers
em nosso código. Quando alteramos o FindUsersBySearchTextQuery
para return em UserInfo[]
vez (por implementação IQuery<UserInfo[]>
), o UserController
falhará ao compilar, já que a restrição de tipo genérico em IQueryHandler<TQuery, TResult>
não será capaz de mapear FindUsersBySearchTextQuery
para User[]
.
IQueryHandler
No 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 IQueryHandlers
com 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 IQueryProcessor
depende da IQuery<TResult>
interface. Isso nos permite ter suporte de tempo de compilação em nossos consumidores que dependem do IQueryProcessor
. Vamos reescrever o UserController
para 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 UserController
agora depende de um IQueryProcessor
que pode lidar com todas as nossas consultas. O UserController
's SearchUsers
método chama o IQueryProcessor.Process
método passando um objeto de consulta inicializado. Como o FindUsersBySearchTextQuery
implementa 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 Process
método também é conhecido.
Agora é responsabilidade da implementação do IQueryProcessor
encontrar 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 QueryProcessor
classe 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 Handle
mé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 TQuery
argumento genérico não está disponível no momento da compilação. No entanto, a menos que o Handle
mé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.