c# - generic - repository pattern php
Bien diseƱados comandos de consulta y/o especificaciones (4)
He estado buscando bastante tiempo para una buena solución a los problemas presentados por el típico patrón Repositorio (creciente lista de métodos para consultas especializadas, etc. consulte: http://ayende.com/blog/3955/repository-is-the-new-singleton ).
Realmente me gusta la idea de usar consultas de Comando, particularmente a través del 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 trata los otros problemas de consultas, como unirse, agrupar, seleccionar o proyectar subconjuntos, etc. básicamente, todos los aros adicionales que deben pasar muchas consultas para obtener el conjunto correcto de datos.
(nota: uso 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 de comando / consulta donde hay una distinción entre consultas y comandos (actualizar, eliminar, insertar))
Así que estoy buscando alternativas que encapsulen toda la consulta, pero aún lo suficientemente flexibles como para no estar intercambiando repositorios de spaghetti por una explosión de clases de comando.
He usado, por ejemplo, Linqspecs, y aunque encuentro valioso el poder asignar nombres significativos a los criterios de selección, simplemente no es suficiente. Tal vez estoy buscando una solución combinada que combine múltiples enfoques.
Estoy buscando soluciones que otros puedan haber desarrollado ya sea para abordar este problema, o abordar un problema diferente, pero aún así cumple con estos requisitos. En el artículo vinculado, Ayende sugiere utilizar el contexto nHibernate directamente, pero creo que eso complica en gran medida su capa de negocio porque ahora también debe contener información de consulta.
Voy a ofrecer una recompensa por esto, tan pronto como el período de espera transcurra. Así que, por favor, hagan que sus soluciones valgan la pena, con buenas explicaciones, y seleccionaré la mejor solución, y subiré de nivel 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 cabrían mejor. Si se puede adaptar fácilmente a otros ORM, sería una bonificación. 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 de Repository. La mayoría de mis aplicaciones no son lo suficientemente complejas como para garantizar CQRS (algo con lo que la mayoría de los defensores de CQRS dicen fácilmente que no deberías usarlo).
ACTUALIZACIÓN: Parece que hay un poco de confusión aquí. No estoy buscando una nueva tecnología de acceso a datos, sino una interfaz razonablemente bien diseñada entre negocios y datos.
Idealmente, lo que estoy buscando es una especie 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 trata con el aspecto cláusula where, y no con los otros aspectos de la consulta, como uniones, sub-selecciones, etc. Los repositorios manejan 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 repositorios con explosiones de objetos de consulta.
Hice esto, apoyé esto y deshice esto.
El principal problema es este: no importa cómo lo hagas, la abstracción agregada no te gana 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 de modelo.
La parte divertida es que respondiste tu propia pregunta en respuesta a la respuesta de Olivier: "esto es esencialmente duplicar la funcionalidad de Linq sin todos los beneficios que obtienes de Linq".
Pregúntese: ¿cómo podría no ser?
Mi forma de lidiar con eso es realmente simplista y ORM agnóstico. Mi punto de vista para un repositorio es este: 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 el método de repositorio con 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 son detalles sobre cómo, no el qué y los criterios deberían ser solo la base para construir una cláusula where.
Modelo = el objeto final o estructura de datos necesaria para la aplicación.
public class MyCriteria
{
public Guid Id {get;set;}
public string Name {get;set;}
//etc
}
public interface Repository
{
MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria);
}
Probablemente puede usar los criterios ORM (Nhibernate) directamente si lo desea. La implementación del repositorio debe saber cómo usar 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 construya la consulta en sí misma. El modelo cambia tanto que no puedes definir algo estable?
Esta solución requiere claramente un código adicional, pero no combina el resto de un ORM o lo que sea que esté utilizando para acceder al almacenamiento. El repositorio hace su trabajo para actuar como fachada e IMO está limpio y el código de ''traducción de criterios'' es reutilizable
Puedes usar una interfaz fluida. La idea básica es que los métodos de una clase devuelven la instancia actual esta misma clase después de haber realizado alguna acción. Esto le permite encadenar llamadas de método.
Al crear una jerarquía de clases apropiada, puede crear un flujo lógico de métodos accesibles.
public class FinalQuery
{
protected string _table;
protected string[] _selectFields;
protected string _where;
protected string[] _groupBy;
protected string _having;
protected string[] _orderByDescending;
protected string[] _orderBy;
protected FinalQuery()
{
}
public override string ToString()
{
var sb = new StringBuilder("SELECT ");
AppendFields(sb, _selectFields);
sb.AppendLine();
sb.Append("FROM ");
sb.Append("[").Append(_table).AppendLine("]");
if (_where != null) {
sb.Append("WHERE").AppendLine(_where);
}
if (_groupBy != null) {
sb.Append("GROUP BY ");
AppendFields(sb, _groupBy);
sb.AppendLine();
}
if (_having != null) {
sb.Append("HAVING").AppendLine(_having);
}
if (_orderBy != null) {
sb.Append("ORDER BY ");
AppendFields(sb, _orderBy);
sb.AppendLine();
} else if (_orderByDescending != null) {
sb.Append("ORDER BY ");
AppendFields(sb, _orderByDescending);
sb.Append(" DESC").AppendLine();
}
return sb.ToString();
}
private static void AppendFields(StringBuilder sb, string[] fields)
{
foreach (string field in fields) {
sb.Append(field).Append(", ");
}
sb.Length -= 2;
}
}
public class GroupedQuery : FinalQuery
{
protected GroupedQuery()
{
}
public GroupedQuery Having(string condition)
{
if (_groupBy == null) {
throw new InvalidOperationException("HAVING clause without GROUP BY clause");
}
if (_having == null) {
_having = " (" + condition + ")";
} else {
_having += " AND (" + condition + ")";
}
return this;
}
public FinalQuery OrderBy(params string[] fields)
{
_orderBy = fields;
return this;
}
public FinalQuery OrderByDescending(params string[] fields)
{
_orderByDescending = fields;
return this;
}
}
public class Query : GroupedQuery
{
public Query(string table, params string[] selectFields)
{
_table = table;
_selectFields = selectFields;
}
public Query Where(string condition)
{
if (_where == null) {
_where = " (" + condition + ")";
} else {
_where += " AND (" + condition + ")";
}
return this;
}
public GroupedQuery GroupBy(params string[] fields)
{
_groupBy = fields;
return this;
}
}
Lo llamarías así
string query = new Query("myTable", "name", "SUM(amount) AS total")
.Where("name LIKE ''A%''")
.GroupBy("name")
.Having("COUNT(*) > 2")
.OrderBy("name")
.ToString();
Solo puedes 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, el método GroupBy
devuelve un GroupedQuery
que es la clase base de Query
y no tiene un método Where
(el método where se declara en Query
). Por lo tanto, no es posible llamar a Where
después de GroupBy
.
Sin embargo, no es perfecto. Con esta jerarquía de clases, puede ocultar miembros de forma sucesiva, pero no mostrar los nuevos. Por lo tanto, Having
arroja una excepción cuando se llama antes de GroupBy
.
Tenga en cuenta que es posible llamar a Where
varias veces. Esto agrega nuevas condiciones con AND
a las condiciones existentes. Esto hace que sea más fácil construir filtros programáticamente desde condiciones individuales. Lo mismo es posible con Having
.
Los métodos que aceptan listas de campos tienen un parámetro params string[] fields
. Le permite pasar nombres de campo individuales o una matriz de cadenas.
Las interfaces fluidas son muy flexibles y no requieren la creación de muchas sobrecargas de métodos con diferentes combinaciones de parámetros. Mi ejemplo funciona con cadenas, sin embargo, el enfoque puede extenderse a otros tipos. También puede declarar métodos predefinidos para casos especiales o métodos que acepten tipos personalizados. También podría agregar métodos como ExecuteReader
o ExceuteScalar<T>
. Esto le permitiría definir consultas como esta
var reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true })
.Where(new CurrentMonthCondition())
.Where(new DivisionCondition{ DivisionType = DivisionType.Production})
.OrderBy(new StandardMonthlyReportSorting())
.ExecuteReader();
Incluso 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 la base de datos guarde en caché los comandos. Este no es un reemplazo para un mapeador O / R, pero puede ayudarlo en situaciones en las que usted crearía los comandos utilizando una cadena de concatenación simple de lo contrario.
Descargo de responsabilidad: dado que todavía no hay buenas respuestas, decidí publicar una parte de una gran publicación de blog que leí hace un tiempo, copiada casi textualmente. Puedes encontrar la publicación completa del blog here . Asi que aqui esta:
Podemos definir las siguientes dos interfaces:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
IQuery<TResult>
especifica un mensaje que define una consulta específica con los datos que devuelve utilizando el tipo genérico TResult
. Con la interfaz definida anteriormente, podemos definir un mensaje de consulta como este:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
Esta clase define una operación de consulta con dos parámetros, que dará como resultado una matriz de objetos de User
. La clase que maneja este mensaje se puede definir de la siguiente manera:
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();
}
}
Ahora podemos permitir que los consumidores dependan de la interfaz genérica IQueryHandler
:
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);
}
}
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 en el UserController
(y todos los demás consumidores de esa interfaz).
La IQuery<TResult>
nos brinda soporte en tiempo de compilación cuando especificamos o IQueryHandlers
en nuestro código. Cuando cambiamos FindUsersBySearchTextQuery
para devolver UserInfo[]
lugar (implementando IQuery<UserInfo[]>
), el UserController
no compilará, ya que la restricción de tipo genérico en IQueryHandler<TQuery, TResult>
no podrá asignar FindUsersBySearchTextQuery
a User[]
.
Sin embargo, inyectar la interfaz IQueryHandler
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:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
IQueryProcessor
es una interfaz no genérica con un método genérico. Como puede ver en la definición de la interfaz, IQueryProcessor
depende de la IQuery<TResult>
. Esto nos permite tener soporte de tiempo de compilación en nuestros consumidores que dependen de IQueryProcessor
. Vamos a reescribir el UserController
para usar el nuevo 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);
}
}
El UserController
ahora depende de un IQueryProcessor
que puede manejar todas nuestras consultas. El método SearchUsers
UserController
llama al método IQueryProcessor.Process
que pasa en un objeto de consulta inicializado. Como FindUsersBySearchTextQuery
implementa la IQuery<User[]>
, podemos pasarla al Execute<TResult>(IQuery<TResult> query)
genérico Execute<TResult>(IQuery<TResult> query)
. Gracias a la inferencia de tipo C #, el compilador puede determinar el tipo genérico y esto nos ahorra tener que indicar explícitamente el tipo. El tipo de devolución del método de Process
también es conocido.
Ahora es responsabilidad de la implementación de IQueryProcessor
encontrar el IQueryHandler
correcto. Esto requiere un tipado dinámico y, opcionalmente, el uso de un marco de Inyección de Dependencia, y todo se puede hacer con unas pocas líneas 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);
}
}
La clase QueryProcessor
construye un tipo específico IQueryHandler<TQuery, TResult>
función del tipo de instancia de consulta suministrada. Este tipo se usa para solicitar a la clase de contenedor suministrada que obtenga una instancia de ese tipo. Lamentablemente, necesitamos llamar al método Handle
mediante la reflexión (utilizando la palabra clave dymamic C # 4.0 en este caso), porque en este momento es imposible convertir la instancia del controlador, ya que el argumento TQuery
genérico no está disponible en tiempo de compilación. Sin embargo, a menos que se Handle
método Handle
u obtenga otros argumentos, esta llamada nunca fallará y, si lo desea, es muy fácil escribir una prueba unitaria para esta clase. Usar la reflexión dará una ligera caída, pero no hay nada de qué preocuparse realmente.
Para responder a una de sus inquietudes:
Así que estoy buscando alternativas que encapsulen toda la consulta, pero aún lo suficientemente flexibles como para no estar intercambiando repositorios de spaghetti por una explosión de clases de comando.
Una consecuencia del uso de 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. De modo que todavía obtiene muchas menos clases de consulta que métodos en un repositorio.