He estado buscando durante bastante tiempo una buena solución a los problemas que presenta el patrón de repositorio típico (lista creciente de métodos para consultas especializadas, etc., consulte: http://ayende.com/blog/3955/repository- es-el-nuevo-singleton ).
Me gusta mucho la idea de utilizar consultas de comandos, especialmente mediante el uso del patrón de especificación. Sin embargo, mi problema con la especificación es que solo se relaciona con los criterios de selecciones simples (básicamente, la cláusula where), y no se ocupa de las otras cuestiones de las consultas, como unión, agrupación, selección o proyección de subconjuntos, etc. Básicamente, todos los aros adicionales por los que deben pasar muchas consultas para obtener el conjunto correcto de datos.
(nota: utilizo el término "comando" como en el patrón de comando, también conocido como objetos de consulta. No estoy hablando de comando como en la separación comando / consulta donde se hace una distinción entre consultas y comandos (actualizar, eliminar, insertar))
Así que estoy buscando alternativas que encapsulen toda la consulta, pero que sigan siendo lo suficientemente flexibles como para que no solo esté intercambiando repositorios de espaguetis por una explosión de clases de comando.
He usado, por ejemplo, Linqspecs, y aunque encuentro algo de valor en poder asignar nombres significativos a los criterios de selección, simplemente no es suficiente. Quizás estoy buscando una solución combinada que combine múltiples enfoques.
Estoy buscando soluciones que otros puedan haber desarrollado para abordar este problema o abordar un problema diferente, pero aún satisfacen estos requisitos. En el artículo vinculado, Ayende sugiere usar el contexto nHibernate directamente, pero creo que eso complica en gran medida su capa empresarial porque ahora también debe contener información de consulta.
Ofreceré una recompensa por esto, tan pronto como pase el período de espera. Por lo tanto, haga que sus soluciones sean dignas de recompensa, con buenas explicaciones, seleccionaré la mejor solución y votaré a los finalistas.
NOTA: Estoy buscando algo basado en ORM. No tiene que ser EF o nHibernate explícitamente, pero esos son los más comunes y encajarían mejor. Si se puede adaptar fácilmente a otros ORM, sería una ventaja. Linq compatible también sería bueno.
ACTUALIZACIÓN: Estoy realmente sorprendido de que no haya muchas buenas sugerencias aquí. Parece que las personas son totalmente CQRS o están completamente en el campo del repositorio. La mayoría de mis aplicaciones no son lo suficientemente complejas como para garantizar CQRS (algo que la mayoría de los defensores de CQRS dicen que no debes usarlo).
ACTUALIZACIÓN: Parece haber un poco de confusión aquí. No busco una nueva tecnología de acceso a datos, sino una interfaz razonablemente bien diseñada entre el negocio y los datos.
Idealmente, lo que estoy buscando es algún tipo de cruce entre los objetos de consulta, el patrón de especificación y el repositorio. Como dije anteriormente, el patrón de especificación solo se ocupa del aspecto de la cláusula where, y no de los otros aspectos de la consulta, como combinaciones, sub-selecciones, etc. Los repositorios se ocupan de toda la consulta, pero se salen de control después de un tiempo. . Los objetos de consulta también se ocupan de toda la consulta, pero no quiero simplemente reemplazar los repositorios con explosiones de objetos de consulta.
fuente
Respuestas:
Descargo de responsabilidad: dado que aún no hay buenas respuestas, decidí publicar una parte de una excelente publicación de blog que leí hace un tiempo, copiada casi literalmente. Puede encontrar la publicación completa del blog aquí . Asi que aqui esta:
Podemos definir las siguientes dos interfaces:
Los
IQuery<TResult>
especifica un mensaje que define una consulta específica con los datos que devuelve usando elTResult
tipo genérico. Con la interfaz definida previamente podemos definir un mensaje de consulta como este:Esta clase define una operación de consulta con dos parámetros, lo que dará como resultado una matriz de
User
objetos. La clase que maneja este mensaje se puede definir de la siguiente manera:Ahora podemos permitir que los consumidores dependan de la
IQueryHandler
interfaz genérica :Inmediatamente este modelo nos da mucha flexibilidad, porque ahora podemos decidir qué inyectar en el
UserController
. Podemos inyectar una implementación completamente diferente, o una que envuelva la implementación real, sin tener que hacer cambios enUserController
(y en todos los demás consumidores de esa interfaz).La
IQuery<TResult>
interfaz nos brinda soporte en tiempo de compilación al especificar o inyectarIQueryHandlers
en nuestro código. Cuando cambiamos elFindUsersBySearchTextQuery
regresarUserInfo[]
lugar (mediante la implementaciónIQuery<UserInfo[]>
), laUserController
dejará de recopilar, ya que el tipo de restricción genérica sobreIQueryHandler<TQuery, TResult>
no será capaz de asignarFindUsersBySearchTextQuery
aUser[]
.IQueryHandler
Sin embargo, inyectar la interfaz en un consumidor tiene algunos problemas menos obvios que aún deben abordarse. El número de dependencias de nuestros consumidores puede ser demasiado grande y puede llevar a una sobreinyección del constructor, cuando un constructor toma demasiados argumentos. El número de consultas que ejecuta una clase puede cambiar con frecuencia, lo que requeriría cambios constantes en el número de argumentos del constructor.Podemos solucionar el problema de tener que inyectar demasiados
IQueryHandlers
con una capa extra de abstracción. Creamos un mediador que se ubica entre los consumidores y los manejadores de consultas:El
IQueryProcessor
es una interfaz no genérico con un método genérico. Como puede ver en la definición de la interfaz,IQueryProcessor
depende de laIQuery<TResult>
interfaz. Esto nos permite tener soporte de tiempo de compilación en nuestros consumidores que dependen delIQueryProcessor
. Reescribamos elUserController
para usar el nuevoIQueryProcessor
:El
UserController
ahora depende de unIQueryProcessor
que pueda manejar todas nuestras consultas. ElUserController
'sSearchUsers
método llama alIQueryProcessor.Process
método que pasa en un objeto de consulta inicializado. Dado queFindUsersBySearchTextQuery
implementa laIQuery<User[]>
interfaz, podemos pasarla alExecute<TResult>(IQuery<TResult> query)
método genérico . Gracias a la inferencia de tipos de C #, el compilador puede determinar el tipo genérico y esto nos ahorra tener que indicar explícitamente el tipo.Process
También se conoce el tipo de retorno del método.Ahora es responsabilidad de la implementación de
IQueryProcessor
encontrar el derechoIQueryHandler
. Esto requiere algo de escritura dinámica y, opcionalmente, el uso de un marco de inyección de dependencia, y todo se puede hacer con solo unas pocas líneas de código:La
QueryProcessor
clase construye unIQueryHandler<TQuery, TResult>
tipo específico basado en el tipo de instancia de consulta proporcionada. Este tipo se utiliza para pedir a la clase de contenedor proporcionada que obtenga una instancia de ese tipo. Desafortunadamente, necesitamos llamar alHandle
método usando la reflexión (usando la palabra clave dinámica C # 4.0 en este caso), porque en este punto es imposible lanzar la instancia del controlador, ya que elTQuery
argumento genérico no está disponible en tiempo de compilación. Sin embargo, a menosHandle
que se cambie el nombre del método o se obtengan otros argumentos, esta llamada nunca fallará y, si lo desea, es muy fácil escribir una prueba unitaria para esta clase. El uso de la reflexión producirá una ligera caída, pero no es nada de lo que preocuparse.Para responder a una de sus inquietudes:
Una consecuencia de usar este diseño es que habrá muchas clases pequeñas en el sistema, pero tener muchas clases pequeñas / enfocadas (con nombres claros) es algo bueno. Este enfoque es claramente mucho mejor que tener muchas sobrecargas con diferentes parámetros para el mismo método en un repositorio, ya que puede agruparlos en una clase de consulta. Por lo tanto, todavía obtiene muchas menos clases de consulta que métodos en un repositorio.
fuente
TResult
parámetro genérico de laIQuery
interfaz no es útil. Sin embargo, en mi respuesta actualizada,TResult
elProcess
método deIQueryProcessor
para resolver elIQueryHandler
en tiempo de ejecución usa el parámetro .IQueryable
y asegurándome de no enumerar la colección, luego desde elQueryHandler
acabo de llamar / encadenar las consultas. Esto me dio la flexibilidad de realizar pruebas unitarias de mis consultas y encadenarlas. Tengo un servicio de aplicación además de miQueryHandler
, y mi controlador está a cargo de hablar directamente con el servicio en lugar del controladorMi forma de lidiar con eso es en realidad simplista y agnóstica de ORM. Mi punto de vista para un repositorio es el siguiente: el trabajo del repositorio es proporcionar a la aplicación el modelo requerido para el contexto, por lo que la aplicación solo le pide al repositorio lo que quiere, pero no le dice cómo obtenerlo.
Proporciono al método de repositorio un Criterio (sí, estilo DDD), que será utilizado por el repositorio para crear la consulta (o lo que sea necesario, puede ser una solicitud de servicio web). Las uniones y grupos en mi humilde opinión son detalles de cómo, no el qué y un criterio debería ser solo la base para construir una cláusula de dónde.
Modelo = el objeto final o la estructura de datos que necesita la aplicación.
Probablemente pueda usar los criterios ORM (Nhibernate) directamente si lo desea. La implementación del repositorio debe saber cómo utilizar los Criterios con el almacenamiento subyacente o DAO.
No conozco su dominio y los requisitos del modelo, pero sería extraño si la mejor manera es que la aplicación cree la consulta. ¿El modelo cambia tanto que no se puede definir algo estable?
Esta solución claramente requiere un código adicional, pero no acopla el resto a un ORM o lo que sea que esté usando para acceder al almacenamiento. El repositorio hace su trabajo para actuar como una fachada y, en mi opinión, está limpio y el código de 'traducción de criterios' es reutilizable
fuente
Hice esto, apoyé esto y deshice esto.
El principal problema es este: no importa cómo lo hagas, la abstracción agregada no te otorga independencia. Se filtrará por definición. En esencia, está inventando una capa completa solo para hacer que su código se vea lindo ... pero no reduce el mantenimiento, mejora la legibilidad ni le otorga ningún tipo de agnosticismo en el modelo.
La parte divertida es que respondió su propia pregunta en respuesta a la respuesta de Olivier: "esto es esencialmente duplicar la funcionalidad de Linq sin todos los beneficios que obtiene de Linq".
Pregúntate: ¿cómo no podría ser?
fuente
Puede utilizar una interfaz fluida. La idea básica es que los métodos de una clase devuelven la instancia actual de esta misma clase después de haber realizado alguna acción. Esto le permite encadenar llamadas a métodos.
Al crear una jerarquía de clases adecuada, puede crear un flujo lógico de métodos accesibles.
Lo llamarías así
Solo puede crear una nueva instancia de
Query
. Las otras clases tienen un constructor protegido. El objetivo de la jerarquía es "deshabilitar" los métodos. Por ejemplo, elGroupBy
método devuelve aGroupedQuery
which es la clase base deQuery
y no tiene unWhere
método (el método where está declarado enQuery
). Por lo tanto, no es posible llamarWhere
despuésGroupBy
.Sin embargo, no es perfecto. Con esta jerarquía de clases, puede ocultar miembros sucesivamente, pero no mostrar nuevos. Por lo tanto,
Having
lanza una excepción cuando se llama antesGroupBy
.Tenga en cuenta que es posible llamar
Where
varias veces. Esto agrega nuevas condicionesAND
a las condiciones existentes. Esto facilita la construcción de filtros mediante programación a partir de condiciones únicas. Lo mismo es posible conHaving
.Los métodos que aceptan listas de campos tienen un parámetro
params string[] fields
. Le permite pasar nombres de campo único o una matriz de cadenas.Las interfaces fluidas son muy flexibles y no requieren que crees muchas sobrecargas de métodos con diferentes combinaciones de parámetros. Mi ejemplo funciona con cadenas, sin embargo, el enfoque se puede extender a otros tipos. También puede declarar métodos predefinidos para casos especiales o métodos que acepten tipos personalizados. También puede agregar métodos como
ExecuteReader
oExceuteScalar<T>
. Esto le permitiría definir consultas como estaIncluso los comandos SQL construidos de esta manera pueden tener parámetros de comando y así evitar problemas de inyección de SQL y al mismo tiempo permitir que el servidor de base de datos almacene en caché los comandos. Esto no es un reemplazo para un mapeador O / R, pero puede ayudar en situaciones en las que crearía los comandos usando una simple concatenación de cadenas de otro modo.
fuente