Август 2015

Том 30 выпуск 8

На переднем крае - CQRS и события: мощный дуэт

Дино Эспозито

Dino EspositoКак и в случае многих аспектов современного ПО, на самом деле ничто не ново. Зачастую просто меняются названия и презентуются новыми модными словами. Отличный пример — Command Query Responsibility Segregation (CQRS). Другой пример — события предметной области, которые описывают наблюдаемые изменения в предметной области бизнеса.

Определяя язык программирования Eiffel в конце 1980-х, Бертран Мейер (Bertrand Meyer) сформулировал принцип на основе того, что сегодня называют CQRS. В программном обеспечении любая базовая операция может быть либо командой, либо запросом, но не командой и запросом. Если это команда, ожидается, что она изменяет состояние системы. Если это запрос, ожидается, что он сообщит о состоянии системы, ни в малейшей степени не изменяя его.

Иначе говоря, задание вопроса не должно менять ответ. Это иногда называют принципом разделения команд и запросов (Command Query Separation, CQS). С командами и запросами все ясно, а как насчет событий предметной области (domain events)?

События в бизнес-приложениях

С первых дней разработки ПО архитекторы проектировали специализированные бизнес-приложения (line-of-business applications), способные выдерживать изменения в бизнесе и отслеживать эти изменения. Для поддержки бизнес-анализа и статистического анализа архитекторы также умудрялись создавать последовательности в какой-то мере повторяемых операций. Они не называли их событиями предметной области и не выдумывали модных терминов вроде источников событий, тем не менее концепция событий предметной области активно использовалась годами в бизнес-системах.

Потом произошла революция под названием «разработка, управляемая предметной областью» (Domain Driven Design, DDD), и, как я полагаю, заблудившись в этой «новой» универсальной модели ПО, все утратили некую перспективу в отношении архитектуры ПО. Разработчики больше концентрировались на уровнях и не принимали во внимание вертикальные сегменты систем, такие как стеки команд и запросов.

На мой взгляд, разница между принципом CQS, сформулированным Бертраном Мейером, и нынешним принципом CQRS заключается лишь в том, где они применяются. CQS является в большей мере универсальным принципом разработки ПО. CQRS затрагивает архитектуру системы. Применяя CQS на уровне архитектуры, вы получаете CQRS.

Введение в CQRS я впервые дал в статье «CQRS for the Common Applications» (msdn.microsoft.com/magazine/mt147237). И немного углубился в нее в последующей статье — «CQRS and Message-based Applications» (msdn.microsoft.com/magazine/mt238399).

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

Больше внимания уделялось использованию сообщений для реализации рабочих бизнес-процессов, инициируемых командами. В этом контексте события были просто уведомлениями между обработчиками о недавних «происшествиях», на которые обработчикам, возможно, нужно реагировать. Это первый шал в более длительной эволюции, направленной на то, чтобы архитекторы ПО перешли от идеи сохранения моделей к идее протоколирования событий в журналах. Я вижу CQRS как начальную точку перемен, которые окажут глубокое влияние на архитектуру систем.

Яблоко сэра Исаака Ньютона и я

Все слышали об истории с яблоком, которое упало на голову сэра Исаака Ньютона, после чего тот сформулировал Закон всемирного тяготения. Это больше похоже на легенду, чем на факт, но многие открытия совершенно точно начинаются с какого-то любопытного события. Недавно со мной случилась собственная версия падения яблока на голову, которая заставила меня серьезно поразмыслить о CQRS и событиях.

Я занимался кое-каким предварительным анализом с целью переработки системы, использовавшейся заказчиком примерно пять лет. Система этого заказчика представляла собой веб-сайт ASP.NET MVC 2 с чистой базой данных SQL Server. Уровень доступа к данным был ориентирован на реляционную базу данных, используемую через Entity Framework. Логически распознаваемые классы сущностей были расширены дополнительными, специфичными для данного бизнеса метода, используя механизм частичных классов в Microsoft .NET Framework. Поведение между сущностями делегировалось сервисам предметной области. Хотя я не назвал бы это эталонной реализацией шаблона Domain Model, в ней там и сям присутствовали многие части DDD. В целом, я назвал бы это чистой системой CRUD (Create, Read, Update, Delete) с некоторым количеством бизнес-логики, размещенной как буфер между серверной стороной и остальной частью системы.

В программном обеспечении CRUD-система — это приложение, опирающееся на набор таблиц базы данных. Уровни, окружающие базу данных, проверяют и закладывают основу для CRUD-операций с базой данных. Такова была эта система.

Другой аспект CRUD-системы, который зачастую упускают из виду или на который просто не обращают должного внимания: использует ли она базу данных как снимок. В любой момент база данных, стоящая за CRUD-системой, хранит последнее известное нормальное состояние системы. Иногда этого достаточно, иногда нет.

Яблоко, упавшее мне на голову, заставило меня осознать, что подход со снимком базы данных был корректен для первой версии. Во второй версии заказчик хочет большего. Теперь ему нужна возможность отслеживания хронологии и повторения последовательностей шагов в разных условиях для определения результата. Когда дело доходит до таких требований, снимок базы данных как единственной (или основной) формы хранилища больше не годится.

Пример приложения, резервирующего помещения для совещаний

Рассмотрим приложение, позволяющее резервировать помещения для совещаний. В некий момент презентационный уровень покажет экран, аналогичный представленному на рис. 1.

Имитация журнала учета использования помещений
Рис. 1. Имитация журнала учета использования помещений

Вообразите, что для каждого помещения действуют свои бизнес-правила, касающиеся резервирования. Например, пользователь может зарезервировать помещение White на один час с 8 до 10 утра, а затем с 13 дня. В помещении Blue доступны получасовые интервалы с 9:30 утра. Наконец, помещение Green доступно для использования по часу с 9 утра. Эти подробности сохраняются и соответствуют существующими резервированиям. Можно сгенерировать сетку, где уже занятые интервалы времени закрашиваются серым, а свободные интервалы можно щелкать.

Правила управления доступностью помещений не являются незыблемыми и могут меняться. Подумаешь, скажете вы. Если доступность меняется, пусть администратор соответственно поправит правила. Этот подход годится, если вы используете имитацию на рис. 1 для представления самого последнего состояния системы.

В тот момент, когда заказчик попросит сделать возможной переход между текущей сеткой и сеткой трехнедельной давности, вы пропали. Точнее, вы пропали, если в течение последних трех недель вносились какие-либо изменения в бизнес-правила резервирования помещений. Вам известны временные показатели для каждого помещения, но вы утратили детали того, что было три недели назад.

Когда заказчик поднял этот вопрос, я подумал, что для устранения проблемы будет достаточно помещать в таблицу некоторую недостающую информацию. Мне нужно было лишь сохранять некоторые правила и снова считывать их при необходимости. Как бы просто это ни звучало, должная забота о такой малой части недостающей информации требует глубокого изменения в архитектуре. Это как вершина айсберга — и айсберг переключается со снимка базы данных на представление данных на основе событий.

Понятие источника событий относится к представлению данных на основе событий. Я не буду здесь углубляться в источники событий, но они точно станут главной темой следующей статьи в этой рубрике. В остальной части этой статьи я рассмотрю смешанный подход, который связывает снимки базы данных с журналами бизнес-событий. По-видимому, это хороший первый шаг в переходе существующих классических архитектур к будущему. Большой бум на следующие несколько лет будет главным образом в области архитектуры: данные на основе событий как основной источник данных вместо снимков данных (рис. 2).

Подводная часть айсберга событий
Рис. 2. Подводная часть айсберга событий

Database as State Snapshot База данных как снимок состояния
CRUD CRUD
We Are Here Мы находимся здесь
Event-Based Representation of Data Представление данных на основе событий
Event Sourcing Источники событий

За границами снимков данных

Десятилетиями многие из нас создавали системы, где внимание уделялось только самому последнему, текущему состоянию системы. В этом сценарии базы данных хранили снимки сущностей (или агрегаты, если вам комфортнее с терминологией DDD), и все прекрасно работало. Если вы никогда не чувствовали потребности вести журналы бизнес-событий по мере их появления, то, вероятно, она и не возникнет.

Но, как только ваш заказчик запросит функциональность, для поддержки которой нужно переводить «стрелки часов» в приложении назад к конкретному моменту, ваша спокойная жизнь закончилась. Единственный выход — перепроектировать архитектуру системы для сохранения бизнес-событий. Допустим, что теперь у вас есть таблица, аналогичная показанной на рис. 3.

Пример таблицы, хранящей правила резервирования помещений
Рис. 3. Пример таблицы, хранящей правила резервирования помещений

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

Запись в таблице RoomRules логически эквивалентна бизнес-событию в том плане, что она сообщает о событии, которое должно изменить правила резервирования. Однако в терминах реализации это просто запись в дополнительной таблице, и перед заполнением сетки на рис. 1 требуется ряд дополнительных запросов (по одному на помещение).

Новое правило резервирование требует лишь новой записи в таблице. Однако усердные разработчики, по-видимому, уже располагают той или иной таблицей правил. Они предоставляли бы по одной записи на помещение. В этом контексте единственной разницей кажется дополнительный столбец под названием ValidSince на рис. 3.

Использование словосочетания «бизнес-событие», пришедшего из классического видения снимков хранилищ, для определения группы дополнительных записей в дополнительной таблице может показаться перебором. Тем не менее, это именно та перспектива, которую вы получаете, когда находитесь прямо на ватерлинии со снимками сверху и целым миром источников событий под вами.

В более специфических терминах правила резервирования помещений являются событиями, и их запись в журналы — это отслеживание истории системы. Получение правил для применения к конкретному помещению является «воспроизведением» событий для этого помещения, который происходили с момента запуска системы вплоть до данной точки. Вы можете не сохранять где-то текущее состояние, но у вас записаны в журналы все события. Чтобы получить состояние системы, просто воспроизведите события и создайте правильный экземпляр сущности Room.

Это звучит странно? Именно так любое банковское приложение вычисляет ваш текущий баланс. Баланс никогда не является результатом простого чтения. Скорее всего это снимок, сохраненный на определенную дату, с воспроизведением всех последующих событий.

События и CQRS в единой связке

События настолько пронизывают программное обеспечение, что даже трудно вообразить. Они затрагивают даже чистые CRUD-системы, как только заказчики просят простые формы бизнес- и статистического анализа. События неизменяемы. Ввиду этого вы можете легко реплицировать базы данных событий в новые экземпляры программного обеспечения. Это главное условие масштабируемости. В то же время разделение команд и запросов (а значит, и наличие разных стеков для обработки команд и выполнения запросов) позволяет вам сосредоточиться на событиях в стеке команд и держать снимки таблиц наготове для быстрого чтения в стеке запросов.

Если у вас есть проблемы с производительностью, вы должны сохранять события при выполнении какой-либо команды и обновлении всех проекций данных, нужных для запросов. Например, когда вы добавляете новое правило резервирования, такое сохранение с этого момента должно выполняться по умолчанию. Вы можете обновить дополнительную таблицу текущих настроек, чтобы быстро запросить их без воспроизведения событий. Потом вы можете вернуться и воспроизвести события так, как это требуется.

Заключение

Используя события, вы ничего не пропустите. Пока ваш бизнес работает по-прежнему, начинайте осваивать более мощный тип архитектуры. Сохраняя данные как события, вы спускаетесь на один шаг ниже уровня абстракции данных, т. е. вы можете использовать тот же объем данных более низкого уровня для формирования любого количества проекций и снимков для многих бизнес-целей — чистых запросов, бизнес-анализа, анализа «что-если», статистики, анализа использования и чего угодно, что только можно вообразить.


Дино Эспозито (Dino Esposito) — соавтор книг «Microsoft .NET: Architecting Mobile Applications Solutions for the Enterprise» (Microsoft Press, 2014) и «Programming ASP.NET MVC 5» (Microsoft Press, 2014). Идеолог в области технологий для платформ .NET Framework и Android в JetBrains. Часто выступает на конференциях по всему миру, делится своим видением ПО на software2cents.wordpress.com и пишет заметки в twitter.com/despos.

Выражаю благодарность за рецензирование статьи эксперту Джону Арну Сетересу (Jon Arne Saeteras).