Реализация CQRS в .NET. Часть 1 – шаблоны CQS и CQRS

Давайте разберемся что кроется за аббревиатурами 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.