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:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
Los IQuery<TResult>
especifica un mensaje que define una consulta específica con los datos que devuelve usando el TResult
tipo genérico. Con la interfaz definida previamente 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, lo que dará como resultado una matriz de User
objetos. 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 IQueryHandler
interfaz 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);
}
}
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 UserController
(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 inyectar IQueryHandlers
en nuestro código. Cuando cambiamos el FindUsersBySearchTextQuery
regresar UserInfo[]
lugar (mediante la implementación IQuery<UserInfo[]>
), la UserController
dejará de recopilar, ya que el tipo de restricción genérica sobre IQueryHandler<TQuery, TResult>
no será capaz de asignar FindUsersBySearchTextQuery
a User[]
.
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:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
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 la IQuery<TResult>
interfaz. Esto nos permite tener soporte de tiempo de compilación en nuestros consumidores que dependen del IQueryProcessor
. Reescribamos 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 pueda manejar todas nuestras consultas. El UserController
's SearchUsers
método llama al IQueryProcessor.Process
método que pasa en un objeto de consulta inicializado. Dado que FindUsersBySearchTextQuery
implementa la IQuery<User[]>
interfaz, podemos pasarla al Execute<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 derecho IQueryHandler
. 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:
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 QueryProcessor
clase construye un IQueryHandler<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 al Handle
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 el TQuery
argumento genérico no está disponible en tiempo de compilación. Sin embargo, a menos Handle
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:
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.
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.