Застереження: Оскільки поки немає чудових відповідей, я вирішив опублікувати частину чудового допису в блозі, який я прочитав деякий час тому, скопійований майже дослівно. Повний допис у блозі ви можете знайти тут . Ось воно:
Ми можемо визначити наступні два інтерфейси:
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
};
// Note how we omit the generic type argument,
// but still have type safety.
User[] users = this.queryProcessor.Process(query);
return this.View(users);
}
}
UserController
Тепер залежить на IQueryProcessor
який може обробляти всі наші запити. Метод UserController
'' 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
метод, використовуючи відображення (за допомогою ключового слова C # 4.0 dymamic у цьому випадку), оскільки на даний момент неможливо привести екземпляр обробника, оскільки загальний TQuery
аргумент недоступний під час компіляції. Однак, якщо Handle
метод не перейменований або не отримає інших аргументів, цей виклик ніколи не провалиться, і якщо ви цього захочете, дуже легко написати модульний тест для цього класу. Використання роздумів дасть невелике падіння, але насправді не про що турбуватися.
Щоб відповісти на одне із ваших проблем:
Тож я шукаю альтернативи, які б інкапсулювали весь запит, але все ж досить гнучкі, щоб ви не просто міняли сховища спагеті на вибух командних класів.
Наслідком використання цієї конструкції є те, що в системі буде багато малих класів, але мати багато малих / цілеспрямованих класів (з чіткими назвами) - це добре. Цей підхід, очевидно, набагато кращий, ніж багато перевантажень з різними параметрами для одного і того ж методу в сховищі, оскільки ви можете згрупувати їх в одному класі запитів. Отже, ви все одно отримуєте набагато менше класів запитів, ніж методи у сховищі.