7/9/2011 3:26:54 PM

Целая история с этим CQRS.

Из моих заметок, то, что Udi старательно отмечал, подчёркивал и всячески делал акцент:

  • CQRS – это не паттерн для дизайна целой системы, это не паттерн масштаба системы!
  • CQRS – это не “best practice”!
  • CQRS “работает” внутри одного BC.
  • CQRS – паттерн для решения проблем, связанных с совместной работой нескольких пользователей.
  • CQRS используется там, где имеются высоко конкурентные данные (highly-collaborative domains, но я избегаю термина “домен” здесь и в баззе – с ним возникает путаница, этим термином что только не называют: “домен”, “доменную модель”, “анемичную модель”, “набор POCO, который мы назваем “домен”, “entities из Entity Framework” и и.д, так что, простите за абсолютно неправильный перевод того, что сказал Udi и приведение всего этого к “данным”, ибо в конечном итоге именно данные конкурентны в highly-collaborative domains).
  • CQRS не нужен там, где происходит работа с неконкурентными данными (профиль пользователя, свой бид в системе аукциона, описание или название продукта и т.д.)

CQRS НЕ применим в случаях, когда:

  • Запросы не имеют права возвращать устаревшие данные.
    Иными словами, при использовании CQRS запросы всегда будут возвращать устаревшие данные. То есть, может случиться так, что данные будут свежими, но это вопрос удачи, ибо CQRS применяется для работы с высоко конкурентными данными, а это в общем случае означает, что “за то время, что прошло от выполнения запроса и до того момента, пока начнётся работа с данными, эти данные успеют много раз поменяться” и “сейчас мы читаем эти данные, но в настоящий момент они меняются”, это понятно.
  • Команды имеют достаточно большой шанс на неудачу.

Иными словами, хорошая команда – это команда, на которую мы можем ответить “Большое спасибо, если что-то не так – мы Вам сообщим” :) И в 99.99% ничего не сообщить.

Отправка команды

В формировании “хорошей” команды нам поможет “агрессивная валидация” на этапе ввода команды, которая может быть выполнена на основании той Persisted ViewModel, которая быстра и находится близко к клиенту, и о которой я писал в предыдущий раз.

После такой валидации у нас есть все основания полагать, что шанс на то, что команда закончится неудачей с точки зрения валидации чрезвычайно мал.

Racing Conditions не существуют

Если с валидацией всё совсем просто, то с бизнес-правилами разговор отдельный.
И вот здесь включается ещё одно правило CQRS, которое обычно воспринимается как “best practice”, хотя на самом деле имеет под собой гораздо бОльшие основания.

Это правило, которое говорит нам о том, что команда должна быть контекстно-зависимой, должна “олицетворять” собой намерения пользователя, а не указание сделать что-то на данном этапе.

Это очень важно, потому что (сводим воедино):

  • Мы находимся в highly collaborative domain (конкурентные данные).
  • У нас существуют бизнес-правила, чётко описывающие бизнес-процесс и способные привести к решению конечной задачи, поставленной пользователем.
  • Racing conditions в мире бизнеса не существует.

Последний пункт нуждается в пояснении.
Простой пример: мы можем отменить заказ клиента до тех пор, пока товар не отправлен клиенту.
Это не очень-то уж и конкурентные данные, но для примера подойдёт. Как раз в тот момент, когда пользователь нажимает “отменить заказ” товар может перейти в состояние “отправлен”. Просто пример, для иллюстрации того, как две команды в конкурентной среде, выданные на основании заведомо устаревших данных могут поступить в систему в одно время и состязаться в скорости исполнения на предмет “кто первый успеет в борьбе за ресурс”.

Можно изобретать различные хитроумные блокировки.
Можно жаловаться на отсутствие referential integrity (я не знаю, чем она тут может помочь, но люди смотрят и жалуются), так как мы находимся в распределённой системе и отмену заказа осуществляет сервис Sales или Finance, а за отправку отвечает сервис Shipping, а они обмениваются между собой сообщениями через очереди, поэтому не особо-то и поблокируешь.

А можно пойти к бизнесу и спросить: что делать, если клиент захотел отменить заказ, но как раз в ту же секунду его пометили как отправленный? Как часто бывает такая ситуация?

Можно получить удивительные ответы, например:
- Ну, в тех 0.01%, когда такое случается, будем считать в пользу клиента!
- Не парься, в 0.01% случаев, когда это произойдёт, мы свяжемся с клиентом по имейл и решим проблему!
- В случае, когда товар уже отправлен, мы можем вернуть деньги за вычетом стоимости пересылки!
- Мы вернём деньги, но если товар уже отправлен, то только тогда, когда товар вернётся к нам назад, а если нет – то сразу!
- Мы будем де-факто биллить клиента только после того, как товар доставлен ему, так что проблем с рефандом у нас нет, в этом случае нам нужно просто обратиться к фидексу, либо узнать цену, если товар уже едет (и тогда снять с клиента только эту сумму), либо сделать отмену пересылки в фидексе – и никто никому ничего не должен!

Это и есть бизнес-правила. В мире бизнеса есть бизнес-правила, рефанд-полиси, процессы. А racing conditions нет.
Есть пользователь, нажавший кнопку “Отменить заказ” и получивший в ответ “спасибо, Ваш запрос будет рассмотрен в ближайшее время, мы сообщим результат на Ваш имейл”. И дальше – обработка в соответствие с бизнес-правилами. И не нужно изобретать никаких даблчеков (check-lock-check) с проверкой статусов и прочего всякого.

Чего хотел пользователь?

Как уже было отмечено, требование CQRS к тому, чтобы команда явно выражала намерения пользователя (и чем точнее – тем лучше) – это не просто наглядность. Это требование моделирования и не только тесно связано как с бизнес-правилами, но и влияет на другие аспекты системы.

Предположим простую ��итуацию: бронирование мест в кинотеатре, или на стадионе, или даже в самолёте.
Очень часто такие системы предлагают пользователю простой и понятный интерфейс: карту посадочных мест, где пользователь может выбрать себе место. Или несколько мест, если он планирует смотреть/болеть/лететь с друзьями.
Обычная система, обычный интерфейс, интуитивно понятный, придуманный 15 лет назад и проверенный временем, ничего страшного пока не происходит.
Человек видит, какие места уже заняты, отмечает чекбоксами свободные места на четырёх человек, нажимает кнопку.
И вот тут “страшное” начинает происходить. Мы ведь находимся в highly collaborative domain, конкурентные данные.
Экран обновляется, человек видит ошибку: одно из мест, которое он выбрал, уже занято. Человек выбирает другие четыре места рядом, нажимает кнопку – та же история.
Не очень приятный опыт, не очень хороший пользовательский интерфейс. Лишняя нагрузка на систему – человек постоянно кликает, а продвинуться не может.
Но это мы, ITшники придумали такой и дали бизнесу. Это мы, программисты, придумали блокировки и racing conditions, которых в мире бизнеса просто нет.
Если обратить внимание на то, как работает бизнес, то можно заметить, что он подстраивается под наши системы и находит способы обходить такие “косяки”.
Например, кассиры, работающие на продаже билетов в кинотеатре, договариваются между собой: “ты продаёшь чётные ряды, я – нечётные”, или “твои – с A до F, мои – с G до K” и т.д. В их мире нет racing conditions.
Когда человек покупает больше одного билета, они обычно спрашивают: “вы бы хотели сидеть вместе?” – и предлагают человеку соответствующие варианты.

Это и называется “учитывать намерения пользователя”. Пользователь не хочет выбирать какие-то конкретные места в зале, он хочет четыре места рядом. Это и есть контекст и намерение пользователя.

Ещё маленький пример, приведённый Udi и хорошо иллюстрирующий ситуацию: лифты. В небоскрёбах обычно работает много людей и там бывает много лифтов. Выглядит это обычно так: пользователь подходит к лифту, нажимает кнопку “вверх” или “вниз”, указывая, куда он хочет ехать, после чего какое-то продолжительное время (особенно в начале рабочего дня) ждёт лифта, потом в лифт заходят много народу и начинают выходить на каждом этаже. Лифтов всегда не хватает, скорость человекопотока маленькая.
Потом кто-то додумался: пусть люди не просто нажимают кнопку “вверх” или “вниз”, а сразу вводят этаж, на который они хотят попасть. Тогда система может анализировать эту информацию и оптимизировать поток в “реальном” времени. Человек выбрал этаж – ему на табло высветилось “Вам к лифту D”. В результате лифт D едет до 42 этажа без остановок, потом делает несколько остановок, снова забирает людей и т.д.
Простое учитывание намерений людей – и там, где не хватало 10 лифтов, с лихвой справляются 4.

Так же и с нашим примером про места.
Если не показывать человеку карту посадочных мест, а послать в систему команду “нужно билетов на группу в размере 4 человек”, то система будет в состоянии найти 4 свободных места рядом, когда человек нажмёт кнопку и показать карту человеку уже как свершившийся факт: вы будете сидеть здесь, нажимай “заплатить”. 
Если нет мест на одном ряду, то система может предложить “два впереди, два сзади”. Нет таких – “через проход”.
Если билетов нужно не 4, а 20 – то система может рассмотреть вариант бронирования групп по 4-5 находящихся рядом мест.
А если ему заранее предложить выбрать на карте мест некую область, в которой он хотел бы получить места (сзади, в середине, спереди, сбоку), он будет просто счастлив!
Если человеку что-то не понравится – то он вернётся в браузере назад и попробует ещё раз, авось “выпадут” места получше. Однако, в конкурентной среде не всегда можно получить то, что хочется.

Это всё – бизнес-правила, которые легко задаются и проверяются без необходимости вовлекать в это клиента.
Это улучшает “прозрачность” системы, user experience, уменьшает нагрузку на систему.

Но для этого нужно всё время задумываться: чего именно мы хотим добиться? Чего хочет пользователь? Какая именно бизнес-ситуация сложилась? Какая именно проблема решается?

Но это заставляет так же и пересматривать и пользовательский интерфейс, и модель взаимодействия пользователя с системой, делать её ориентированной на задачу (task oriented), а не просто на данные (data oriented). Заставляет моделировать именно бизнес-ситуации, а не entities в базе данных.
Тогда мы можем “скормить” задачу системе и эффективно получить оптимальный на данный момент результат. И получить отдачу от подхода, который называется CQRS.

Для этого в CQRS такое правило, не для того, чтобы в лог записать имя класса-команды и радоваться, что мы знаем, что это было :)

Обратно к реализации или CQRS в действии
ВАЖНО: Мы находимся внутри одного BC!

Итак, картинка получается следующая:

cqrs

Жёлтая стрелка – это Q-часть CQRS, то есть, запрос. Здесь всё точно так же, как и было раньше, без всякого CQRS: из хранилища Persisted ViewModel, находящегося близко к клиенту, достаётся информация и отдаётся пользователю.

В C-случае, в случае команды, она точно так же, как и в предыдущем сценарии, проходит валидацию, после чего “начинает работать”: бизнес-правила там всякие и т.д.

Результат выполнения, кстати, можно так же отразить в Persistent ViewModel, всё равно оттуда будут читать (мы находимся в пределах BC!). Сделать это можно через некий ModelUpdater, который приведёт записываемые данные к виду одной или нескольких ViewModels, находящихся в БД, и который я нарисовал на диаграмме.

Промежуточный итог

Теперь вдох-выдох, что получилось.
”Простые” AC читают и пишут в Persisted ViewModel.
Query-часть CQRS тоже ничем не отличается – ничего не пишет по определению, читает из Persisted ViewModel.
Command-часть CQRS ничего не читает по определению, но тоже пишет в Persisted ViewModel.

Вопрос: зачем нам нужно писать все те же данные ещё и в “главную” базу данных? Даже если это “всего лишь” база одного-единственного BC?
Получается, что не за чем… То есть, мы можем констатировать факт, что все данные для BC у нас хранятся в Persisted ViewModel.
То есть, мы изначально храним данные в виде, готовом для чтения, возможно с какой-то там денормализацией, возможно в разных типах баз данных (NoSQL, Relational) для разных BC… Так, как нам нужно где нам нужно.
База находится максимально близко к клиенту (быть может прямо на веб-сервере), оптимизирована для чтения, своевременно обновляется.

Для меня такая трансформация была неожиданной, у меня даже вырвалось: “Udi, постой, ты же сказал, что это просто база для быстро доступного кеша?! Как это не надо хранить данные в основной базе?!” :)
Но с фактами не поспоришь: база есть, готова к работе, смысла держать ту же информацию где-то ещё, да ещё с учётом того, что, как видно из диаграммы, оттуда никто ничего не читает, крайне не много.

Однако, что же у нас остаётся в “той самой” базе данных? Которая на картинке помечена, как DB, которая в CQRS называется как Command Database?
И что такое эти все бизнес-правила? Как они обрабатываются?

Ответ на этот вопрос можно дать одним словом: Sagas. Но об этом – в следующий раз. Как обычно, может быть. :)

Tags:

Comments are closed

Powered by BlogEngine.NET 2.5.0.6

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter

Widget Twitter not found.

Root element is missing.X


Recent posts

Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2012

Sign in