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 TResulttipo 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 Userobjetos. 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 IQueryHandlerinterfaz 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 IQueryHandlersen nuestro código. Cuando cambiamos el FindUsersBySearchTextQueryregresar UserInfo[]lugar (mediante la implementación IQuery<UserInfo[]>), la UserControllerdejará de recopilar, ya que el tipo de restricción genérica sobre IQueryHandler<TQuery, TResult>no será capaz de asignar FindUsersBySearchTextQuerya User[].
IQueryHandlerSin 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 IQueryHandlerscon 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 IQueryProcessores una interfaz no genérico con un método genérico. Como puede ver en la definición de la interfaz, IQueryProcessordepende de la IQuery<TResult>interfaz. Esto nos permite tener soporte de tiempo de compilación en nuestros consumidores que dependen del IQueryProcessor. Reescribamos el UserControllerpara 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 UserControllerahora depende de un IQueryProcessorque pueda manejar todas nuestras consultas. El UserController's SearchUsersmétodo llama al IQueryProcessor.Processmétodo que pasa en un objeto de consulta inicializado. Dado que FindUsersBySearchTextQueryimplementa 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. ProcessTambién se conoce el tipo de retorno del método.
Ahora es responsabilidad de la implementación de IQueryProcessorencontrar 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 QueryProcessorclase 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 Handlemé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 TQueryargumento genérico no está disponible en tiempo de compilación. Sin embargo, a menos Handleque 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.