Давайте разберемся что кроется за аббревиатурами CQS и CQRS.
Немного теории
Шаблон Command-Query Separation (CQS) предлагает разделение работы с объектом (и это не обязательно база данных) на Запросы (Query) и Команды (Commands). При этом необходимо соблюдать следующие правила:
- Запросы возвращает данные и, что важно, никогда не меняют состояние объекта;
- Команды изменяют состояние объекта, но, в идеальном случае, не должны ничего возвращать.
Из этого следует, что во время отсутствия Команд:
- одинаковые Запросы гарантировано вернут одинаковый результат;
- любое количество любых Запросов не изменят состояние объекта;
- удаление Запроса из кода абсолютно прозрачно для объекта и не может дать побочных действий.
Эти свойства CQS используются в программировании по контракту. В нем методы, осуществляющие проверку состояния объекта, являются Запросами. Это гарантирует их прозрачность для остального кода и возможность безопасного удаления части из них в Release версии кода (например, для оптимизации).
Кроме того, подход, предлагаемый CQS, позволяет сделать код приложения более понятным именно благодаря разделению Команд и Запросов. Соответственно, в дальнейшем такое приложение легче поддерживать и модифицировать.
Необходимо отметить, что в некоторых случаях CQS не может быть использован в принципе. В качестве примеров можно рассмотреть реализации алгоритмов стек и очередь. Их методы Pop() и Dequeue() по определению должны и возвращать данные и изменять состояние самого объекта. Кроме того, в случае многопоточного программирования иногда удобнее иметь методы, противоречащие принципам CQS.
Область использования CQS достаточно широка. Например, он подходит для взаимодействия с элементами управления пользовательского интерфейса. Однако, в случае применения данного принципа для работы с источниками данных используется термин Command-Query Responsibility Segregation (CQRS). По сути это синоним, уточняющий область применения. CQRS несет в себе все принципы CQS и не ограничивает способ хранения данных. Это может быть СУБД, массив в памяти и т. д.
Пример
На небольшом примере посмотрим как может выглядеть реализация CQRS. Допустим, необходимо хранить список ссылок:
public class Link
{
public int Id { get; set; }
public string Url { get; set; }
}
Реализуем Запрос, возвращающий ссылку по ее Id. В конструкторе передадим источник данных, а метод Execute() будет возвращать нужный объект.
public class GetUrlByIdQuery
{
private readonly IEnumerable<Link> _dataSource;
public GetUrlByIdQuery(IEnumerable<Link> dataSource)
{
this._dataSource = dataSource;
}
public Link Execute(int id)
{
return this._dataSource.FirstOrDefault(e => e.Id == id);
}
}
Обратите внимание, что знания о источнике данных в классе запроса сведены к минимуму. Он сможет работать как, например, с обычным массивом, так и с MS SQL через Entity Framework.
Аналогично поступим и при реализации Команды (для упрощения она не будет потокобезопасностной).
public class AddLinkCommand
{
private readonly ICollection<Link> _dataSource;
public AddLinkCommand(ICollection<Link> dataSource)
{
this._dataSource = dataSource;
}
public void Execute(string url)
{
int maxId = this._dataSource.Max(e => e.Id) + 1;
this._dataSource.Add(new Link() {
Id = maxId,
Url = url
});
}
}
Здесь стоит обратить внимание на следующие моменты:
- в метод Execute() передается строка. Таким образом в Команду передаются только необходимые данные (а не объект целиком). Кроме того, вызывающий метод теперь не обязан знать о классе Link. Все это увеличивает гибкость использования Команд.
- Команда содержит часть логики приложения (вычисляет новый Id). Ограничений в шаблоне нет.
- В данном примере для упрощения Команда сама вносит изменения в источник данных. Однако, здесь может быть применен шаблон проектирования Команда (когда Команда только хранит данные для её выполнения, а сам изменения выполняются сторонним обработчиком).
Прейдём к примеру использования:
class Program
{
static void Main(string[] args)
{
var dataSource = new List<Link>() {
new Link() {Id = 1, Url = "http://msdn.microsoft.com/"}
};
Console.WriteLine("* Начальный список ссылок: ");
foreach (var urlData in dataSource)
Console.WriteLine("{0}. {1}", urlData.Id, urlData.Url);
var query = new GetUrlByIdQuery(dataSource);
var link = query.Execute(1);
Console.WriteLine("\n* Результат запроса:\n{0}. {1}", link.Id, link.Url);
var cmd = new AddLinkCommand(dataSource);
cmd.Execute("http://www.windowsazure.com/");
Console.WriteLine("\n* Список ссылок после выполнения команды: ");
foreach (var urlData in dataSource)
Console.WriteLine("{0}. {1}", urlData.Id, urlData.Url);
Console.ReadKey(true);
}
}
Используя CQRS в реальном приложении, можно добиться того, что бизнес-логика не будет ничего знать про тип источника данных. Она будет оперировать только Запросами и Командами. При правильном подходе, это может существенно упростить создание и дальнейшее сопровождение приложения.
В завершении этой части необходимо сказать, что CQS/CQRS не ограничиваются способы их реализации. Это могут быть методы одного объекта или отдельные классы, как в примере выше. Возможно использовать совместно интерфейсы, абстракции, IoC и прочие техники и шаблоны.
В следующей части рассмотрим работу с библиотекой Highway, реализующий шаблон CQRS для .NET.