Отказ от ответственности: поскольку пока нет хороших ответов, я решил опубликовать часть из отличного сообщения в блоге, которое я прочитал некоторое время назад, скопированный почти дословно. Вы можете найти полную запись в блоге здесь . Итак, вот оно:
Мы можем определить следующие два интерфейса:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
В IQuery<TResult>определяет сообщение , которое определяет конкретный запрос с данными возвращаемой с помощью TResultуниверсального типа. С помощью ранее определенного интерфейса мы можем определить сообщение запроса следующим образом:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
Этот класс определяет операцию запроса с двумя параметрами, результатом которой будет массив Userобъектов. Класс, обрабатывающий это сообщение, можно определить следующим образом:
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();
}
}
Теперь мы можем позволить потребителям зависеть от универсального 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);
}
}
Эта модель сразу же дает нам большую гибкость, потому что теперь мы можем решить, что вводить в UserController. Мы можем внедрить совершенно другую реализацию или ту, которая обертывает реальную реализацию, без необходимости вносить изменения в UserController(и всех других потребителей этого интерфейса).
IQuery<TResult>Интерфейс дает нам время компиляции поддержки при указании или инъекционных IQueryHandlersв нашем коде. Когда мы изменить , FindUsersBySearchTextQueryчтобы вернуться UserInfo[]вместо этого ( за счет реализации IQuery<UserInfo[]>), то UserControllerне будет компилироваться, так как общий тип ограничения на IQueryHandler<TQuery, TResult>не будет в состоянии отобразить FindUsersBySearchTextQueryв User[].
Однако внедрение IQueryHandlerинтерфейса в потребителя имеет несколько менее очевидных проблем, которые все еще необходимо решить. Количество зависимостей наших потребителей может стать слишком большим и может привести к чрезмерному внедрению конструктора - когда конструктор принимает слишком много аргументов. Количество запросов, выполняемых классом, может часто меняться, что потребует постоянного изменения количества аргументов конструктора.
Мы можем решить проблему слишком большого количества инъекций IQueryHandlersс помощью дополнительного уровня абстракции. Мы создаем посредника, который находится между потребителями и обработчиками запросов:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
Это IQueryProcessorнеуниверсальный интерфейс с одним универсальным методом. Как вы можете видеть в определении интерфейса, это IQueryProcessorзависит от IQuery<TResult>интерфейса. Это позволяет нам иметь поддержку времени компиляции в наших потребителях, которые зависят от IQueryProcessor. Давайте перепишем, UserControllerчтобы использовать новый 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
};
User[] users = this.queryProcessor.Process(query);
return this.View(users);
}
}
UserControllerТеперь зависит на IQueryProcessorкоторый может обрабатывать все наши запросы. В UserController«S SearchUsersметод вызывает IQueryProcessor.Processметод , проходящий в инициализированном объекте запроса. Поскольку FindUsersBySearchTextQueryреализует IQuery<User[]>интерфейс, мы можем передать его универсальному Execute<TResult>(IQuery<TResult> query)методу. Благодаря выводу типа C # компилятор может определять универсальный тип, и это избавляет нас от необходимости явно указывать тип. ProcessТакже известен возвращаемый тип метода.
Теперь IQueryProcessorпоиск правильных решений является обязанностью реализации IQueryHandler. Это требует некоторой динамической типизации и, возможно, использования инфраструктуры внедрения зависимостей, и все это может быть выполнено с помощью всего нескольких строк кода:
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);
}
}
QueryProcessorКласс создает определенный IQueryHandler<TQuery, TResult>тип , основанный на типе экземпляра прилагаемого запроса. Этот тип используется, чтобы запросить предоставленный класс контейнера для получения экземпляра этого типа. К сожалению, нам нужно вызвать Handleметод с помощью отражения (в данном случае с использованием ключевого слова dymamic C # 4.0), потому что на этом этапе невозможно привести экземпляр обработчика, поскольку универсальный TQueryаргумент недоступен во время компиляции. Однако, если Handleметод не будет переименован или не получит другие аргументы, этот вызов никогда не завершится ошибкой, и если вы захотите, очень легко написать модульный тест для этого класса. Использование отражения даст небольшое снижение, но не о чем беспокоиться.
Чтобы ответить на одну из ваших проблем:
Поэтому я ищу альтернативы, которые инкапсулируют весь запрос, но при этом достаточно гибкие, чтобы вы не просто меняли местами спагетти-репозитории для взрыва класса команд.
Следствием использования этого дизайна является то, что в системе будет много маленьких классов, но наличие большого количества маленьких / сфокусированных классов (с понятными именами) - это хорошо. Этот подход явно намного лучше, чем наличие множества перегрузок с разными параметрами для одного и того же метода в репозитории, поскольку вы можете сгруппировать их в один класс запроса. Так что классов запросов по-прежнему гораздо меньше, чем методов в репозитории.