Принцип CQRS¶
«Приказ формулируется ясно, кратко и четко без употребления формулировок, допускающих различные толкования».
«Устав внутренней службы ВС РФ».
CQRS (Command & Query Responsibility Segregation) — принцип разделения запросов на чтение данных и команд на изменение данных. В соответствии с принципом CQRS не должно быть запросов, одновременно и изменяющих, и считывающих данные. Или иначе — данные не должны модифицироваться во время подготовки ответа на запрос; формирование ответа не должно иметь побочных эффектов.
Применение принципа CQRS позволяет упростить программу, так как код становится более читаемым, отладка - более прозрачной, предсказуемой и повторяемой.
Сам по себе CQRS не является глубокомысленной архитектурной парадигмой, это всего лишь достаточно простой шаблон, применение которого хорошо сочетается с большим количеством самых разнообразных архитектур. Конкретная имплементация принципа зависит от архитектуры компонентов, используемых в системе, и может вылиться, например, в разделение очередей запросов данных и команд изменения данных.
Хотя CQRS является не какой-то самодостаточной архитектурной сущностью, а скорее концепцией или «советом» по поводу общих принципов проектирования, тем не менее, вдумчивое применение CQRS способно привести к достаточно глубокому изменению архитектурных принципов построения приложения.
Рассмотрим, например, следующий пример. Скажем, у нас есть сайт, отображающий курсы валют, бэкенд бизнес-логика которого имеет следующие методы:
# CurrencyInterface
def GetCurrency(CurrencyID) -> Currency: # Работа с одиночным значением
def SetCurrency(CurrencyID, Currency) -> None:
def GetCurrencyList(CurrencyID, datetime start, datetime end) -> List[Currency]: # Работа с последовательностью данных
def SetCurrencyList(List[Currency]) -> None:
Вроде всё выглядит «нормально» — мы можем читать и писать как одиночные значения, так и последовательности данных. На первый взгляд, данные на запись и на чтение - это, можно сказать, одни и те же данные, и работа с ними выглядит достаточно консистентно.
Но, предположим, сайт стал набирать популярность и перед нами стала задача масштабирования архитектуры бэкенда. Сайт очень популярен, и мы развернули бэкенд аж на десяти серверах, каждый из которых, естественно, поддерживает как запись, так и чтение данных.
Что мы сделали не так? Чтобы разобраться, проведем еще один мысленный эксперимент. Сделаем два интерфейса:
# ReadCurrencyInterface
def GetCurrency(CurrencyID) -> Currency:
def GetCurrencyList(CurrencyID, datetime start, datetime end) -> List[Currency]:
# WriteCurrencyInterface
def SetCurrency(CurrencyID, Currency) -> None:
def SetCurrencyList(List[Currency]) -> None:
Сайт набирает популярность, мы снова задумываемся о масштабировании, снова развертываем десять серверов, но... Теперь сам вид наших интерфейсов совершенно отчетливо напоминает нам о том, что стоит масштабировать только интерфейс чтения, в то время как интерфейс записи, доступный исключительно администраторам и служебному ПО, обновляющему информацию на сайте — в масштабировании не нуждается, его вполне можно, как и раньше, запускать на одном-единственном сервере. Это не было секретом и раньше, при работе с первым, совместным интерфейсом, но представление об «общем интерфейсе» и «общих данных» настолько затуманило эту мысль, что вылилось уже в избыточные траты на масштабирование не нуждающегося в этом функционала.
Принцип CQRS приносит осознание того, что обработка команд и запросов бывает принципиально ассиметричной, а симметричное масштабирование сервисов часто не имеет никакого смысла.
Резюмируя, можно сказать, что сам по себе принцип CQRS достаточно банален, просто давая некий ментальный толчок к рассуждению об архитектуре приложения; сложности (и преимущества) будут лежать скорее в области архитектурных решений, выбранных вами для интеграции двух разделённых при помощи CQRS потоков данных.