Паттерн CQRS — руководство для чайников
Рассказываем, что такое паттерн CQRS (Command Query Responsibility Segregation), зачем он нужен и как внедрить его для своего проекта.
22К открытий33К показов
Не так давно в рамках одного из проектов впервые столкнулся с таким понятием, как CQRS. Честно скажу, заинтересовало сразу, потому что в проект очень удобно и просто встроиться, легко понять, что, где и как происходит. Достаточно прочитать одну статью или просмотреть обучающее видео и ты уже “вооружен”, чтобы приступать к работе на проекте.
И сейчас, спустя время, хочу поделиться с читателями издания Tproger своим видением построения проекта по этому паттерну. Начнем с небольшой теории.
Паттерн CQRS (Command Query Responsibility Segregation) – это подход к проектированию системы, который разделяет операции чтения и записи данных на две отдельные модели. Этот подход позволяет улучшить производительность системы и упростить ее сопровождение. Часто на просторах интернета вы можете встретить подобную схему.
Как было сказано, CQRS разделяет операции над данными на две категории: команды, которые вносят изменения в состояние системы и запросы – операции получения данных, без внесения изменений в состояние.
Проще всего это объяснить на примере стандартных CRUD операций. В CQRS операция чтения (Read) будет являться запросом, т.к. с помощью нее получаются данные и ничего более. Остальные же операции (Create, Update, Delete) в данном подходе будут являться командами, которые так или иначе изменяют состояние.
Почему CQRS
Кратко пройдемся по преимуществам данного подхода:
- Простота понимания: разделение операций чтения и записи позволяет создавать более чистый и модульный код.
- Улучшенная производительность: разделение операций чтения и записи позволяет оптимизировать каждую из них для конкретных задач.
- Улучшенная масштабируемость: разделение операций чтения и записи позволяет легко масштабировать каждую из них отдельно.
- Простота тестирования: разделение операций чтения и записи позволяет легко тестировать каждую из них отдельно.
Теперь, разобравшись в теории, что такое CQRS и зачем он нужен, предлагаю перейти к непосредственной практике.
Подготовка проекта
В рамках статьи разберем простейший пример приложения с применением паттерна CQRS. Для этого создадим ASP.NET Core Web API проект на .NET 6.0
В данном примере прибегну к помощи библиотеки MediatR, поэтому добавлю ее в самом начале.
И после этого, согласно документации библиотеки, зарегистрируем ее в контейнере зависимостей.
Перед тем, как приступить к прописыванию логики, создадим класс, описывающий объект товара, назовем его “Product” и именно над объектами данного класса будем производить необходимые операции.
Далее создадим некое подобие контекста базы данных (или репозитория), в котором инкапсулируем работу с объектами класса “Product”. В нашем случае пропишем в этом классе методы для получения списка данных и добавления элемента в этот список.
После этого необходимо зарегистрировать наше хранилище.
Запрос для получения данных
Теперь можно приступить к написанию первых запросов и команд, которые наглядным образом покажут принцип работы с данными в этом подходе. Для начала создадим 3 папки в корне проекта “Queries”, “Commands” и “Handlers”.
Начнем с базового – запроса для получения списка всех продуктов из нашего хранилища. Для этого в папку “Queries” добавим record, который назовем “GetProductsQuery”, он будет реализовывать интерфейс “IRequest” из пространства имен “MediatR”, передав в этот интерфейс параметр, который будет указывать на тип возвращаемых данных при выполнении запроса – в данном случае это “IEnumerable
Далее необходимо создать класс-обработчик указанного запроса. Для этого в папку “Handlers” добавим класс “GetProductsQueryHandler”. Данный класс должен реализовывать“IRequestHandler
В данном обработчике будет прописана логика получения данных из нашего хранилища, т.е. можно грубо провести аналогию с обычным сервисом, который “общается” с репозиторями для получения данных. Для этого необходимо получить экземпляр хранилища, с которым будет вестись работа. Создадим приватное свойство только для чтения типа “ProductStore” и инициализируем его в конструкторе:
Теперь, реализуя интерфейс “IRequestHandler”, создаем метод Handle
В общем виде класс будет выглядеть следующим образом:
Далее пропишем контроллер “ProductsController”. Для отправки команд будем использовать интерфейс “IMediator”, а конкретно его метод Send(), который принимает параметром объект команды. Контроллер с методом для получения списка продуктов будет выглядеть следующим образом:
Запустив приложение можно протестировать работу данного метода через Postman.
Как видим, все работает прекрасно. Теперь можно приступить к написанию команд – операций по изменению данных в хранилище.
Команда для добавления данных в хранилище
Создадим команду для добавления продукта в наше хранилище. Принцип остается тем же: создаем команду, создаем обработчик команды и добавляем метод в наш контроллер.
Отличие команды от запроса заключается в том, что необходимо добавить входной параметр – объект, который будем добавлять, а тип возвращаемого значения мы укажем “Product”, чтобы вернуть новый объект.
Итак, собственно, сама команда:
Обработчик команды по своей структуре абсолютно идентичен обработчику запроса. Единственное, на что стоит обратить внимание – это метод “Handle”. В нем мы берем из хранилища идентификатор крайнего элемента, чтобы дать новому объекту подходящий ID. Наименование товара берем из входного параметра request, который является экземпляром команды “AddProductCommand”. После записи в хранилище, созданный экземпляр с новым ID возвращаем пользователю.
Для того, чтобы вернуть пользователю созданный объект, опишем еще один запрос для получения элемента по его идентификатору.
И добавим необходимые методы в наш контроллер. Первый – HttpGet метод для получения объекта по id, который получаем из строки запроса, также этому методу задаем параметр “Name” для того, чтобы после создания объекта в HttpPost методе по этому параметру переадресовать клиента для получения созданного продукта.
В HttpPost методе стоит обратить внимание на последнюю строку. В ней вызывается метод “CreatedAtAction”, который используется для создания ответа HTTP 201 Created, который содержит ссылку на вновь созданный ресурс. Этот метод принимает три параметра: имя действия, параметры запроса и объект, который будет возвращен в качестве результата действия.
Теперь проверим, как это работает с помощью того же Postman.
Объект был успешно создан и возвращен с новым id. Код ответа – 201 Created и если посмотрим заголовки ответа, то увидим созданный методом CreatedAtAction заголовок Location.
Таким образом, на примере разобрали принцип построения проекта по паттерну CQRS. Данный пример довольно простой и в реальных проектах все намного сложнее, не все разработчики предпочитают использовать библиотеку MediatR, т.к. это замедляет процесс обработки операций, но это уже тема для другой статьи. Здесь же вы могли увидеть наиболее простой для понимания проект, построенный по принципам CQRS.
Итог
Если кратко подытожить, то стоит отметить следующее:
- CQRS – это подход к проектированию, который разделяет операции над данными на две категории: запросы и команды.
- Запросы – это операции получения данных, не изменяющие состояние.
- Команды – это операции изменения состояния системы.
- Класс (или record), описывающий операцию (будь то команда или запрос), должен имплементировать интерфейс IRequest
, где T – тип возвращаемого значения операции. Поля этого класса (или record’a) – входные параметры операции. - Класс-обработчик операции должен реализовывать интерфейс IRequestHandler
, где TCommand – команда, обработчиком которой является данный класс, TResponse – тип возвращаемого значения. Для работы с данными традиционно в этом классе присутствует свойство, представляющее объект репозитория, с которым работает обработчик, этот объект инициализируется в конструкторе, куда он приходит из контейнера зависимостей. - Основная работа с данными производится в методе Handle(TCommand command, CancellationToken token), типом возвращаемого значения которого является Task
. - Вызов операции из контроллера производится методом Send интерфейса IMediator (объект которого нужно получить из контейнера зависимостей). В качестве параметра в метод Send передается объект класса (или record’a), описывающего операцию.
В заключении хочется сказать, что паттерн CQRS является одним из наиболее эффективных подходов к проектированию системы, который позволяет улучшить ее производительность и упростить сопровождение. Если вы хотите создать эффективную и легко сопровождаемую систему, то рассмотрите возможность использования паттерна CQRS.
22К открытий33К показов