Отказ от ответственности: поскольку пока нет хороших ответов, я решил опубликовать часть из отличного сообщения в блоге, которое я прочитал некоторое время назад, скопированный почти дословно. Вы можете найти полную запись в блоге здесь . Итак, вот оно:
Мы можем определить следующие два интерфейса:
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
метод не будет переименован или не получит другие аргументы, этот вызов никогда не завершится ошибкой, и если вы захотите, очень легко написать модульный тест для этого класса. Использование отражения даст небольшое снижение, но не о чем беспокоиться.
Чтобы ответить на одну из ваших проблем:
Поэтому я ищу альтернативы, которые инкапсулируют весь запрос, но при этом достаточно гибкие, чтобы вы не просто меняли местами спагетти-репозитории для взрыва класса команд.
Следствием использования этого дизайна является то, что в системе будет много маленьких классов, но наличие большого количества маленьких / сфокусированных классов (с понятными именами) - это хорошо. Этот подход явно намного лучше, чем наличие множества перегрузок с разными параметрами для одного и того же метода в репозитории, поскольку вы можете сгруппировать их в один класс запроса. Так что классов запросов по-прежнему гораздо меньше, чем методов в репозитории.