Command and Query Objects
Command and Query Objects — архітектурний шаблон проєктування заснований на принципі CQRS
Переваги та недоліки
Переваги
- бізнес-логіка поділяється між об'єктами queries і commands. Код сценарію використання інкапсульований в обробнику
- обробники не містять зайвих залежностей
- легко реалізовувати наскрізну функціональність, оскільки всі обробники мають однаковий інтерфейс
- queries відповідають лише за читання даних, commands — за зміну даних. Зменшується навантаження на вибірку даних
- легко замінити компоненти. Їх регулюванням займається посередник
Недоліки
- важкий в реалізації
- збільшується кількість класів
Опис мовою C#
Додамо деякі класи, які будуть симулювати реальні об'єкти.
public class DB { ... } // доступ до бази даних
public class User { ... } // об'єктно-орієнтоване відображення таблиці в базі даних
Запишемо інтерфейс query та її обробника. Інтерфейс IQuery не містить ніяких визначень. Його будуть реалізовувати дто-класи призначення яких інкапсулювати дані, та передати їх обробникам. Іншими словами, query відіграє роль аргументів функції. Зате, він містить очікуваний результат повернення, таким чином на його обробника накладаються певні обмеження. Обробник для query — IQueryHandler — містить лише один метод — Handle. Він приймає аргументи, та повертає необхідний результат.
public interface IQuery<TResult> { }
public interface IQueryHandler<TQuery, TResult>
where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
Припустимо, що виникла задача, знайти користувачів, за введеним значенням. Тоді Query і обробник можуть мати наступний вигляд:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; private set; }
// конструктор, є обов'язковою частиною
// він гарантує передачу усіх параметрів в Query
public FindUsersBySearchTextQuery(string searchText)
{
this.SearchText = searchText;
}
}
public class FindUsersBySearchTextQueryHandler
: IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
DB db;
public FindUsersBySearchTextQueryHandler(DB db)
{
this.db = db;
}
public User[] Handle(FindUsersBySearchTextQuery query)
{
return db.GetUser(query.SearchText)
}
}
Деколи немає необхідності в породжені великої кількості класів обробників, тоді можна помістити всі обробники в один клас:
public class FindUsersBySearchTextQueryHandler
: IQueryHandler<FindUsersBySearchTextQuery, User[]>,
IQueryHandler<AllUserQuery, User[]>
{
DB db;
public FindUsersBySearchTextQueryHandler(DB db)
{
this.db = db;
}
public User[] Handle(FindUsersBySearchTextQuery query)
{
return db.GetUsers(query.SearchText);
}
public User[] Handle(AllUserQuery query)
{
return db.GetAllUsers();
}
}
Залишилось написати клас, який буде спідставляти для query його handler.
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
// якщо логіка створення обробника є важкою
// тоді цей метод доцільно винести в окремий інтерфейс фабрики
IDictionary<Type, Func<object, object>> RegistrateHandlers();
}
public sealed class QueryProcessor : IQueryProcessor
{
DB db;
IDictionary<Type, Func<object, object>> handlers;
public QueryProcessor(DB db)
{
this.db = db;
this.handlers = RegistrateHandlers();
}
// не обов'язково public
// логіку спідставлення можна не виносити в інтерфейс,
// а приховати від користувача
public IDictionary<Type, Func<object, object>> RegistrateHandlers()
{
IDictionary<Type, Func<object, object>> handlers = new Dictionary<Type, Func<object, object>>();
// спідставлення для запиту відповідного обробника
handlers[typeof(FindUsersBySearchTextQuery)] = (param) => new FindUsersBySearchTextQueryHandler(db).Handle(param as FindUsersBySearchTextQuery);
. . .
return handlers;
}
// виконання обробника, залежно від запиту
public TResult Process<TResult>(IQuery<TResult> query)
{
return (TResult)handlers[query.GetType()].Invoke(query);
}
}
Використання матиме наступний вигляд:
IQueryProcessor queryProcessor = new QueryProcessor(new DB());
User[] filteredUsers = queryProcessor.Process(new FindUsersBySearchTextQuery("search text"));
Розробка command має ту саму структуру. Тільки призначенням command буде не взяття даних, а їх модифікації, створення, видалення тощо.
public interface ICommand<out TResult> { }
public interface ICommandHandler<in TCommand, out TResult> where TCommand : ICommand<TResult>
{
TResult Execute();
}
// можливо клас результату
public class CommandResponse
{
public bool IsSucessed { get; set; }
public string Message { get; set; }
}
public interface IСommandProcessor
{
TResult Process<TResult>(ICommand<TResult> command);
IDictionary<Type, Func<object, object>> RegistrateHandlers();
}