CQRS это довольно хорошо изученная закономерность.
Вы часто будете слышать, что вы либо следуете CQRS, либо нет, подразумевая, что это что-то вроде бинарного выбора.
В этой статье я хотел бы показать, что существует множество вариаций этой концепции, а также то, как различные типы CQRS могут выглядеть на практике.
Тип 0: нет CQRS
При использовании этого типа шаблон CQRS вообще не используется.Это означает, что ваша модель предметной области использует классы предметной области для обслуживания как команд, так и запросов.
Давайте возьмем класс Customer в качестве примера:
С нулевым типом CQRS вы работаете с классом CustomerRepository, который выглядит следующим образом:public class Customer { public int Id { get; private set; } public string Name { get; private set; } public IReadOnlyList<Order> Orders { get; private set; } public void AddOrder(Order order) { /* … */ } /* Other methods */ }
public class CustomerRepository
{
public void Save(Customer customer) { /* … */ }
public Customer GetById(int id) { /* … */ }
public IReadOnlyList<Customer> Search(string name) { /* … */ }
}
Метод поиска здесь — это запрос.
Он используется для получения данных о клиенте из базы данных и возврата этих данных клиенту (который может быть пользовательским интерфейсом или отдельным приложением, обращающимся к вашему приложению через API).
Обратите внимание, что этот метод возвращает список объектов домена.
Преимущество этого подхода заключается в отсутствии накладных расходов на объем кода.
Другими словами, у вас есть одна модель, которую вы можете использовать как для команд, так и для запросов, без необходимости дублировать код. Обратной стороной здесь является то, что эта единственная модель не оптимизирована для операций чтения.
Если вам нужно отобразить список клиентов в пользовательском интерфейсе, обычно вам не нужно отображать их заказы.
Вместо этого в большинстве случаев вам потребуется отображать только краткую информацию, такую как идентификатор, имя и количество заказа.
Использование классов предметной области для транспортировки данных приводит к тому, что все подобъекты клиентов (например, «Заказы») загружаются в память из базы данных.
Это приводит к серьёзным накладным расходам, потому что.
Пользовательскому интерфейсу требуется только количество заказов, а не сами заказы.
Этот тип CQRS хорош для приложений с небольшими (или вообще без) требованиями к производительности.
Для других типов приложений мы должны использовать следующие типы CQRS.
Тип 1: отдельная иерархия классов.
При использовании этого типа CQRS структура классов разделена для обработки операций чтения и записи.
Это означает, что вы создаете набор классов DTO для транспортировки данных, загруженных из базы данных.
DTO для класса Customer может выглядеть так: public class CustomerDto
{
public int Id { get; set; }
public string Name { get; set; }
public int OrderCount { get; set; }
}
Метод Search в репозитории возвращает список DTO вместо списка объектов домена: public class CustomerRepository
{
public void Save(Customer customer) { /* … */ }
public Customer GetById(int id) { /* … */ }
public IReadOnlyList<CustomerDto> Search(string name) { /* … */ }
}
Поиск может использовать как ORM, так и обычный ADO.NET для получения необходимых данных.
Это должно определяться требованиями к производительности в каждом конкретном случае.
Нет необходимости возвращаться к ADO.NET, если производительность метода удовлетворительна.
DTO добавляют некоторое дублирование в том смысле, что теперь нам нужно создать два класса вместо одного: один раз для команд в форме объекта домена и один раз для запросов в формате DTO. В то же время они позволяют нам создавать чистые и понятные структуры данных, которые четко соответствуют потребностям наших операций чтения, поскольку содержат только то, что необходимо для отображения.
И чем чётче мы выражаем свои намерения в коде, тем лучше.
На мой взгляд, этого типа CQRS достаточно для большинства корпоративных приложений, потому что.
он дает довольно хороший баланс между простотой кода и производительностью.
Кроме того, при таком подходе у нас есть некоторая гибкость в выборе инструмента для запросов.
Если производительность метода не критична, мы можем использовать ORM и сэкономить время разработчика.
В противном случае мы можем напрямую использовать ADO.NET (или облегченный ORM, такой как Dapper) и писать сложные и оптимизированные запросы вручную.
Тип 2: индивидуальные модели
Этот тип CQRS использует отдельные модели и набор API для обслуживания запросов на чтение и запись.
Это означает, что помимо DTO мы извлекаем все чтения из нашей модели.
Репозитории теперь содержат только методы, относящиеся к командам: public class CustomerRepository
{
public void Save(Customer customer) { /* … */ }
public Customer GetById(int id) { /* … */ }
}
А логика поиска вынесена в отдельный класс: public class SearchCustomerQueryHandler
{
public IReadOnlyList<CustomerDto> Execute(SearchCustomerQuery query)
{
/* … */
}
}
Этот подход добавляет больше накладных расходов по сравнению с предыдущим с точки зрения объема кода, необходимого для обработки запросов, но это хорошее решение, если у вас большие нагрузки на чтение данных.
Помимо возможности писать оптимизированные запросы, тип 2 позволяет нам легко обернуть часть запроса API в какой-то механизм кэширования или даже переместить этот API на отдельный сервер или группу серверов с настроенным балансировщиком нагрузки.
Это решение отлично подходит для приложений с большой разницей в нагрузках чтения и записи, потому что.
Позволяет операциям чтения хорошо масштабироваться.
Если вы хотите еще большего увеличения производительности чтения, вам нужно перейти к типу 3.
Тип 3: разделенное хранилище
Многие считают этот тип «настоящим» CQRS. Чтобы еще больше масштабировать операции чтения, мы можем использовать отдельное хранилище, оптимизированное под требования нашей системы.
Часто таким хранилищем является база данных NoSQL, например MongoDB, или набор реплик из нескольких экземпляров:
Синхронизация здесь происходит в фоновом режиме и может занять некоторое время.
Такие хранилища называются «эвентуально-согласованными».
Хорошим примером является индексирование данных о клиентах с помощью Elastic Search. Часто мы не хотим использовать полнотекстовый поиск, встроенный в SQL Server, потому что.
он не очень хорошо масштабируется.
Вместо этого мы можем использовать нереляционные хранилища данных, оптимизированные для поиска клиентов.
Наряду с лучшей масштабируемостью операций чтения этот тип CQRS несет в себе самые большие накладные расходы.
Мы не только логически разделяем нашу модель чтения и записи, т.е.
используем для этого отдельные классы и даже сборки, но и разделяем саму базу данных.
Заключение
Существуют различные варианты шаблона CQRS, которые вы можете использовать в своем приложении.Нет ничего плохого в том, чтобы придерживаться типа 1 и не переходить к типам 2 и 3, если тип 1 удовлетворяет требованиям к производительности вашего приложения.
Я хотел бы подчеркнуть этот момент: CQRS — это не бинарный выбор.
Существуют различные варианты между полным отсутствием разделения операций чтения и записи (тип 0) и их полным разделением (тип 3).
Необходимо найти баланс между степенью сегрегации и сложностью, которую она порождает. Баланс необходимо искать в каждом конкретном случае, часто путем нескольких итераций.
Шаблон CQRS не следует использовать просто потому, что «мы можем».
Английская версия статьи:
Типы CQRS Теги: #cqrs #программирование #.NET #C++ #OOP
-
После Патча. Часть 3
19 Oct, 24 -
Вопреки Инструкциям
19 Oct, 24 -
Волхов: По Итогам Года
19 Oct, 24 -
Википедия: Закулисные Войны?
19 Oct, 24 -
Заключенные И Ящики
19 Oct, 24 -
100 Тысяч Посетителей В День, Что Ли? Да!
19 Oct, 24