Venho procurando há algum tempo por uma boa solução para os problemas apresentados pelo padrão típico de Repositório (lista crescente de métodos para consultas especializadas, etc. ver: http://ayende.com/blog/3955/repository- is-the-new-singleton ).
Eu realmente gosto da ideia de usar consultas de comando, particularmente por meio do uso do padrão de especificação. No entanto, meu problema com a especificação é que ela se refere apenas aos critérios de seleções simples (basicamente, a cláusula where), e não lida com as outras questões de consultas, como junção, agrupamento, seleção de subconjunto ou projeção, etc. basicamente, todos os obstáculos extras pelos quais muitas consultas devem passar para obter o conjunto correto de dados.
(nota: eu uso o termo "comando" como no padrão de Comando, também conhecido como objetos de consulta. Não estou falando sobre comando como na separação de comando / consulta, onde há uma distinção entre consultas e comandos (atualizar, excluir, inserir))
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.
Eu usei, por exemplo, Linqspecs, e embora eu ache algum valor em ser capaz de atribuir nomes significativos aos critérios de seleção, não é suficiente. Talvez eu esteja procurando uma solução combinada que combine várias abordagens.
Estou procurando soluções que outros possam ter desenvolvido para resolver esse problema ou resolver um problema diferente, mas ainda satisfaz esses requisitos. No artigo vinculado, Ayende sugere o uso do contexto nHibernate diretamente, mas acho que isso complica muito a sua camada de negócios porque agora também deve conter informações de consulta.
Estarei oferecendo uma recompensa por isso, assim que o período de espera terminar. Então, por favor, faça suas soluções valerem a pena, com boas explicações e eu irei selecionar a melhor solução e votar a favor dos segundos colocados.
NOTA: Estou procurando algo que seja baseado em ORM. Não precisa ser EF ou nHibernate explicitamente, mas esses são os mais comuns e se encaixam melhor. Se puder ser facilmente adaptado a outros ORMs, isso seria um bônus. Compatível com Linq também seria bom.
ATUALIZAÇÃO: Estou realmente surpreso que não haja muitas sugestões boas aqui. Parece que as pessoas são totalmente CQRS ou estão completamente no campo do Repositório. A maioria dos meus aplicativos não é complexa o suficiente para garantir o CQRS (algo com a maioria dos defensores do CQRS prontamente dizem que você não deve usá-lo).
ATUALIZAÇÃO: parece haver um pouco de confusão aqui. Não estou procurando uma nova tecnologia de acesso a dados, mas sim uma interface razoavelmente bem projetada entre negócios e dados.
Idealmente, o que estou procurando é algum tipo de cruzamento entre objetos de consulta, padrão de especificação e repositório. Como eu disse acima, o padrão de especificação lida apenas com o aspecto da cláusula where, e não com os outros aspectos da consulta, como junções, sub-seleções, etc. Os repositórios tratam de toda a consulta, mas ficam fora de controle depois de um tempo . Objetos de consulta também lidam com a consulta inteira, mas não quero simplesmente substituir repositórios por explosões de objetos de consulta.
fonte
Respostas:
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:
O
IQuery<TResult>
especifica uma mensagem que define uma consulta específica com os dados que retorna usando oTResult
tipo genérico. Com a interface definida anteriormente, podemos definir uma mensagem de consulta como esta: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:Agora podemos permitir que os consumidores dependam da
IQueryHandler
interface genérica :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 noUserController
(e em todos os outros consumidores dessa interface).A
IQuery<TResult>
interface nos dá suporte em tempo de compilação ao especificar ou injetarIQueryHandlers
em nosso código. Quando alteramos oFindUsersBySearchTextQuery
para return emUserInfo[]
vez (por implementaçãoIQuery<UserInfo[]>
), oUserController
falhará ao compilar, já que a restrição de tipo genérico emIQueryHandler<TQuery, TResult>
não será capaz de mapearFindUsersBySearchTextQuery
paraUser[]
.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:O
IQueryProcessor
é uma interface não genérica com um método genérico. Como você pode ver na definição da interface, oIQueryProcessor
depende daIQuery<TResult>
interface. Isso nos permite ter suporte de tempo de compilação em nossos consumidores que dependem doIQueryProcessor
. Vamos reescrever oUserController
para usar o novoIQueryProcessor
:O
UserController
agora depende de umIQueryProcessor
que pode lidar com todas as nossas consultas. OUserController
'sSearchUsers
método chama oIQueryProcessor.Process
método passando um objeto de consulta inicializado. Como oFindUsersBySearchTextQuery
implementa aIQuery<User[]>
interface, podemos passá-lo para oExecute<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 doProcess
método também é conhecido.Agora é responsabilidade da implementação do
IQueryProcessor
encontrar o que é certoIQueryHandler
. 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:A
QueryProcessor
classe constrói umIQueryHandler<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 oHandle
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 oTQuery
argumento genérico não está disponível no momento da compilação. No entanto, a menos que oHandle
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:
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.
fonte
TResult
parâmetro genérico daIQuery
interface não é útil. No entanto, em minha resposta atualizada, oTResult
parâmetro é usado peloProcess
método deIQueryProcessor
para resolver oIQueryHandler
em tempo de execução.IQueryable
e certificando-me de não enumerar a coleção, então a partir doQueryHandler
, acabei de chamar / encadear as consultas. Isso me deu a flexibilidade de testar a unidade de minhas consultas e encadeá-las. Eu tenho um serviço de aplicativo além do meuQueryHandler
, e meu controlador é responsável por falar diretamente com o serviço em vez do manipuladorMinha maneira de lidar com isso é realmente simplista e agnóstica de ORM. Minha visão para um repositório é a seguinte: O trabalho do repositório é fornecer ao aplicativo o modelo necessário para o contexto, então o aplicativo apenas pergunta ao repo o que ele deseja, mas não diz como obtê-lo.
Eu forneço o método de repositório com um Criteria (sim, estilo DDD), que será usado pelo repo para criar a consulta (ou o que for necessário - pode ser uma solicitação de serviço da web). Junções e grupos são detalhes de como, não o quê, e os critérios a devem ser apenas a base para construir uma cláusula where.
Model = o objeto final ou estrutura de dados necessária para o aplicativo.
Provavelmente, você pode usar os critérios ORM (Nhibernate) diretamente se quiser. A implementação do repositório deve saber como usar os critérios com o armazenamento subjacente ou DAO.
Não sei o seu domínio e os requisitos do modelo, mas seria estranho se a melhor maneira fosse que o aplicativo construísse a própria consulta. O modelo muda tanto que você não consegue definir algo estável?
Esta solução claramente requer algum código adicional, mas não acopla o resto do a um ORM ou o que você está usando para acessar o armazenamento. O repositório faz seu trabalho como uma fachada e IMO é limpo e o código de 'tradução de critérios' é reutilizável
fonte
Eu fiz isso, apoiei e desfiz isso.
O maior problema é este: não importa como você faça isso, a abstração adicionada não lhe garante independência. Ele vazará por definição. Em essência, você está inventando uma camada inteira apenas para fazer seu código parecer bonito ... mas isso não reduz a manutenção, melhora a legibilidade ou ganha qualquer tipo de agnosticismo de modelo.
A parte divertida é que você respondeu sua própria pergunta em resposta à resposta de Olivier: "isto é essencialmente duplicar a funcionalidade do Linq sem todos os benefícios que você obtém do Linq".
Pergunte a si mesmo: como poderia não ser?
fonte
Você pode usar uma interface fluente. A ideia básica é que os métodos de uma classe retornam a instância atual desta mesma classe depois de executar alguma ação. Isso permite encadear chamadas de método.
Ao criar uma hierarquia de classes apropriada, você pode criar um fluxo lógico de métodos acessíveis.
Você chamaria assim
Você só pode criar uma nova instância de
Query
. As outras classes possuem um construtor protegido. O ponto da hierarquia é "desabilitar" métodos. Por exemplo, oGroupBy
método retorna aGroupedQuery
que é a classe base deQuery
e não tem umWhere
método (o método where é declarado emQuery
). Portanto, não é possível ligarWhere
depoisGroupBy
.No entanto, não é perfeito. Com esta hierarquia de classes, você pode ocultar membros sucessivamente, mas não mostrar novos. Portanto,
Having
lança uma exceção quando é chamado antesGroupBy
.Observe que é possível ligar
Where
várias vezes. Isso adiciona novas condições com umAND
às condições existentes. Isso torna mais fácil construir filtros programaticamente a partir de condições únicas. O mesmo é possível comHaving
.Os métodos que aceitam listas de campos possuem um parâmetro
params string[] fields
. Ele permite que você passe nomes de campo único ou uma matriz de string.As interfaces fluentes são muito flexíveis e não exigem que você crie muitas sobrecargas de métodos com diferentes combinações de parâmetros. Meu exemplo funciona com strings, porém a abordagem pode ser estendida a outros tipos. Você também pode declarar métodos predefinidos para casos especiais ou métodos que aceitam tipos personalizados. Você também pode adicionar métodos como
ExecuteReader
ouExceuteScalar<T>
. Isso permitiria a você definir consultas como estaMesmo os comandos SQL construídos dessa forma podem ter parâmetros de comando e, assim, evitar problemas de injeção de SQL e, ao mesmo tempo, permitir que os comandos sejam armazenados em cache pelo servidor de banco de dados. Isso não é uma substituição para um O / R-mapper, mas pode ajudar em situações em que você criaria os comandos usando concatenação de string simples de outra forma.
fonte