Лекция
Привет, Вы узнаете о том , что такое cqrs, Разберем основные их виды и особенности использования. Еще будет много подробных примеров и описаний. Для того чтобы лучше понимать что такое cqrs, шаблон разделения ответственности, event sourcing, эвент-сорсинг , настоятельно рекомендую прочитать все из категории Разработка программного обеспечения и информационных систем.
The Command and Query Responsibility Segregation (CQRS) - шаблон разделения ответственности на команды и запросы. Разделяет операции чтения и обновления для хранилища данных. Внедрение CQRS в приложение может максимизировать его производительность, масштабируемость и безопасность. Гибкость, создаваемая переходом на CQRS, позволяет системе лучше развиваться с течением времени и не позволяет командам обновления вызывать конфликты слияния на уровне домена.
Шаблон CQRS применяет принцип императивного программирования разделения команд и запросов - сommand-query separation (CQS), используя отдельные объекты запросов и команд для извлечения и изменения данных соответственно . CQS введен Бертраном Мейером во время работы над языком программирования Eiffel. Принцип гласит, что метод должен быть либо командой, выполняющей какое-то действие, либо запросом, возвращающим данные, но не одновременно. Другими словами, задавание вопроса не должно менять ответ. Более формально, возвращать значение можно только чистым (т.е. детерминированным и не имеющим побочных эффектов) методом. Следует отметить, что строгое соблюдение этого принципа делает невозможным отслеживание количества вызовов запросов.
CQRS особенно хорошо вписывается в методологию контрактного программирования, в которой используются утверждения, встроенные в исходный код, описывающие состояние программы в определенные важные моменты. В контрактном программировании утверждения относятся к проектированию, а не к логике выполнения, поэтому их выполнение не должно оказывать влияния на состояние программы. CQRS выгоден для контрактного программирования, так как любой возвращающий значение метод (любой запрос) можно вызывать в утверждениях, не беспокоясь о возможном изменении состояния программы.
Теоретически, это дает возможность узнавать состояние программы, не меняя его. На практике, CQRS дает возможность пропустить все проверки утверждений в действующей системе, чтобы повысить ее производительность, не боясь того, что это изменит ее поведение. CQRS также предотвращает возникновение некоторых гейзенбагов.
Даже за пределами контрактного программирования, применение CQRS рассматривается его приверженцами как оказывающее эффект упрощения на программу, делая доступ к ее состоянию (через запросы) и изменение ее состояния (через команды) более понятными, аналогично тому как избежание использования goto упрощает понимание потока выполнения программы.
CQRS хорошо подходит для методологии объектно-ориентированного программирования, но может быть применен и вне ООП, так как разделение побочных эффектов и возвращения значений не требует ООП, поэтому CQRS может быть с пользой применен в любой парадигме программирования, требующей заботы о побочных эффектах.
CQS может осложнить создание реентерабельного и многопоточного ПО. Эта проблема обычно возникает при использовании непотокобезопасного шаблона для реализации CQS.
Простой пример шаблона, нарушающего CQS, но полезного в многопоточном ПО:
private int x;
public int increment_and_return_x()
{
lock x; // какой-либо механизм блокировки
x = x + 1;
int x_copy = x;
unlock x; // какой-либо механизм блокировки
return x_copy;
}
Распространенный CQS шаблон, применимый только в однопоточных приложениях:
private int x;
public int value()
{
return x;
}
void increment_x()
{
x = x + 1;
}
Даже в случае однопоточных программ иногда можно утверждать, что значительно более удобно иметь метод, объединяющий запрос и команду. Мартин Фаулер приводит метод стека pop() в качестве примера.
мне сразу сказали что на проекте, над которым я буду работать используется CQRS, Event Sourcing, и MongoDB в качестве базы данных. Из этого всего я слышал только о MongoDB. Попытавшись вникнуть в CQRS, я не сразу понял все тонкости данного подхода, но почему-то мне понравилась идея разделения модели взаимодействия с данными на две — read и write. Возможно потому что она как-то перекликалась с парадигмой программирования “разделение обязанностей”, возможно потому что была очень в духе DDD.
Вообще многие говорят о CQRS как о паттерне проектирования. На мой взгляд он слишком сильно влияет на общую архитектуру приложения, что бы называться просто “паттерном проектирования”, поэтому я предпочитаю называть его принципом или подходом. Использование CQRS проникает почти во все уголки приложения.
Сразу хочу уточнить что я работал только со связкой CQRS + Event Sourcing, и никогда не пробовал просто CQRS, так как мне кажется что без Event Sourcing он теряет очень много бенефитов. В качестве CQRS фреймворка я буду использовать наш корпоративный Paralect.Domain. Он чем-то лучше других, чем то хуже. В любом случае советую вам ознакомиться и с остальными. Я здесь упомяну только несколько фреймворков для .NET. Наиболее популярные это NCQRS, Lokad CQRS, SimpleCQRS. Так же можете посмотреть на Event Store Джонатана Оливера с поддержкой огромного количества различных баз данных.
var user = Repository.Get(userId);
На самом же деле репозиторий не берет из базы готовое состояние агрегата UserAR (AR = Aggregate Root), он выбирает из базы все события которые ассоциируются с этим юзером, и потом воспроизводит их по порядку передавая в метод On() агрегата.
Например у класса агрегата UserAR должен быть следующий метод, для того чтобы восстановить в состоянии пользователя его ID
protected void On(User_CreatedEvent created)
{
_id = created.UserId;
}
Из всего состояния агрегата мне нужна только _id юзера, так же я мог бы восстановить состояние пароля, имени и т.д. Об этом говорит сайт https://intellect.icu . Однако эти поля могут быть модифицированы и другими событиями, не только User_CreatedEvent соответственно мне нужно будет обработать их все. Так как все события воспроизводятся по порядку, я уверен, что всегда работаю с последним актуальным состоянием агрегата, если я конечно написал обработчики On() для всех событий которые это состояние изменяют.
Давайте рассмотрим на примере создания пользователя как работает CQRS + Event Sourcing.
Первое что я сделаю — это сформирую и отправлю команду. Для простоты пусть команда создания юзера имеет только самый необходимый набор полей и выглядит следующим образом.
public class User_CreateCommand: Command
{
public string UserId { get; set; }
public string Password { get; set; }
public string Email { get; set; }
}
Пусть вас не смущает имя класса команды, оно не соответствует общепринятым гайдлайнсам, но позволяет сразу понять к какому агрегату она относится, и какое действие выполняется.
var command = new User_CreateCommand { UserId = “1”, Password = “password”, Email = “test@test.com”, }; command.Metadata.UserId = command.UserId; _commandService.Send(command);
Затем мне нужен обработчик этой команды. Обработчику команды обязательно нужно передать ID нужного агрегата, по этому ID он получит агрегат из репозитория. Репозиторий строит объект агрегата следующим образом: берет из базы все события которые относятся к этому агрегату, создает новый пустой объект агрегата, и по порядку воспроизведет полученные события на объекте агрегата.
Но так как у нас команды создания — поднимать из базы нечего, значит создаем агрегат сами и передаем ему метаданные команды.
public class User_CreateCommandHandler: CommandHandler { public override void Handle(User_CreateCommand message) { var ar = new UserAR(message.UserId, message.Email, message.Password, message.Metadata); Repository.Save(ar); } }
Посмотрим как выглядит конструктор агрегата.
public UserAR(string userId, string email, string password, ICommandMetadata metadata): this()
{
_id = userId;
SetCommandMetadata(metadata);
Apply(new User_CreatedEvent
{
UserId = userId,
Password = password,
Email = email
});
}
Так же у агрегата обязательно должен быть конструктор без параметров, так как когда репозиторий воспроизводит состояние агрегата, ему надо сначала создать путой экземпляр, а затем передавать события в методы проекции (метод On(User_CreatedEvent created) является одним из методов проекции).
Немного уточню на счет проекции. Проекция — это воспроизведение состояния агрегата, на основе событий из Event Store, которые относятся к этом агрегату. В примере с пользователем — это все события для данного конкретного пользователя. А агрегате все те же события которые сохраняются через метод Apply, можно обработать во время воспроизведения его состояния. В нашем фреймворке для это достаточно написать метод On(/*EventType arg*/), где EventType — тип события которое вы хотите обработать.
Метод Apply агрегата инициирует отправку событий всем обработчикам. На самом деле события будут отправлены только при сохранение агрегата в репозиторий, а Apply просто добавляет их во внутренний список агрегата.
Вот обработчик события(!) создания пользователя, который записывает в read базу собственно самого пользователя.
public void Handle(User_CreatedEvent message)
{
var doc = new UserDocument
{
Id = message.UserId,
Email = message.Email,
Password = message.Password
};
_users.Save(doc);
}
public class User_ChangePasswordCommandHandler: IMessageHandler { // Конструктор опущен public void Handle(User_ChangePasswordCommand message) { // берем из репозитория агрегат var user = _repository.GetById(message.UserId); // выставляем метаданные user.SetCommandMetadata(message.Metadata); // меняем пароль user.ChangePassword(message.OldPassword, message.NewPassword); // сохраняем агрегат _repository.Save(user); } }
public class UserAR : BaseAR
{
//...
public void ChangePassword(string oldPassword, string newPassword)
{
// Если пароль не совпадает со старым, кидаем ошибку
if (_password != oldPassword)
{
throw new AuthenticationException();
}
// Если все ОК - принимаем ивент
Apply(new User_Password_ChangedEvent
{
UserId = _id,
NewPassword = newPassword,
OldPassword = oldPassword
});
}
// Метод проекции для восстановления состояния пароля
protected void On(User_Password_ChangedEvent passwordChanged)
{
_password = passwordChanged.NewPassword;
}
// Так же добавляем восстановления состояния пароля на событие создания пользователя
protected void On(User_CreatedEvent created)
{
_id = created.UserId;
_password = created.Password;
}
}
}
Полностью рабочий пример на ASP.NET MVC находится здесь.
Базы данных там нету, все храниться в памяти. При желании ее достаточно просто прикрутить. Так же из коробки есть готовая реализация Event Store на MongoDB для хранения ивентов.
Чтобы ее прикрутить достаточно в Global.asax файле заменить InMemoryTransitionRepository на MongoTransitionRepository.
В качестве read модели у меня статическая коллекция, так что при каждом перезапуске данные уничтожаются.
Последнее время получают распространение событийно-ориентированные архитектуры (event-driven architectures) и, в частности, Event Sourcing (порождение событий). Эта вызвано стремлением к созданию устойчивых и масштабируемых модульных систем. В этом контексте довольно часто используется термин “микросервисы”. На мой взгляд, микросервисы — это всего лишь один из способов реализации “ограниченного контекста” (Bounded Context). Очень важно правильно определить границы модулей и в этом помогает стратегическое проектирование (Strategic Design), описанное Эриком Эвансом в Domain Driven Design. Оно помогает вам идентифицировать / обнаружить модули, границы (“ограниченный контекст”) и описать, как эти контексты связаны друг с другом (карта контекстов, ContextMap).
Хотя в книге Эрика Эванса это явно не обозначено, но события предметной области очень хорошо содействуют концепциям DDD. Такие практики как Event Storming Альберто Брандолини смещают акцент у событий с технического на организационный и бизнес-уровень. Здесь мы говорим не о событиях пользовательского интерфейса, таких как щелчок на кнопке (ButtonClickedEvent), а о доменных событиях, которые являются частью предметной области. О них говорят и их понимают эксперты в предметной области. Эти события представляют собой первоочередные концепции и помогают сформировать единый язык (ubiquitous language), с которым будут согласны все участники (эксперты в предметной области, разработчики и т.д.).
Доменные события могут использоваться для взаимодействия между ограниченными контекстами. Предположим, у нас есть интернет-магазин с тремя контекстами: Order (заказ), Delivery (доставка), Invoice (счет).
Рассмотрим событие “Заказ принят” в контексте Order. Контекст Invoice, а также контекст Delivery заинтересованы в отслеживании этого события, так как это событие инициирует некоторые внутренние процессы в этих контекстах.
События полезны, так почему бы не использовать их по максимуму. Это основная идея Event Sourcing. Вы храните состояние агрегата не через обновление его данных (CRUD), а через применение потока событий.
Помимо того, что вы можете воспроизвести события и получить состояние, есть еще одна особенность Event Sourcing: вы бесплатно получаете полный журнал аудита. Поэтому, когда требуется такой журнал, при выборе стратегии хранения обязательно обратите внимание на Event Sourcing.
6. От снимков состояний к событиям
Когда состояние системы хранится в виде снимков состояния — состояние объекта в системе представлено изменяемой строкой или строками в базе данных. При обновлении состояния объекта мы перезаписываем строку новыми значениями.
Когда применяется эвент-сорсинг — состояние хранится в виде последовательности неизменяемых событий. При обновлении состояния объекта нужно сохранить новые события не меняя старые.
Событие в эвент-сорсинге это набор данных описывающий некоторый факт реального мира, когда событие уже произошло, как и в реальном мире, оно уже не может измениться.
В коде это некоторый неизменяемый объект,
Вам может показаться странным, что я сразу перешел от доменных событий к хранению, так как, очевидно, что это концепции разных уровней.
… и вот моя точка зрения: Event Sourcing — это локальное решение, используемое только в одном ограниченном контексте! События Event Sourcing не должны выдаваться наружу во внешний мир! Другие ограниченные контексты не должны знать о способах хранения данных друг друга, и поэтому им не важно, использует ли какой-то контекст Event Sourcing.
Если вы используете Event Sourcing глобально, то вы раскрываете свой уровень хранения.
Способ хранения данных становится вашим публичным API. Каждый раз при внесении изменений в уровень хранения вам придется иметь дело с изменением публичного API.
Я уверен, все согласятся с тем, что плохо, когда разные ограниченные контексты совместно используют данные в (реляционной) базе данных из-за возникающей связанности. Но чем это отличается от Event Sourcing? Ничем. Не имеет значения, используете вы общие события или общие таблицы в базе данных. В обоих случаях вы разделяете детали хранения.
Я все еще утверждаю, что доменные события идеально подходят для взаимодействия между ограниченными контекстами, но эти события не должны быть связаны с событиями, которые используются для Event Sourcing.
Предлагаемое решение очень простое: независимо от того, какой подход вы используете для хранения данных (CRUD или Event Sourcing), вы публикуете доменные события в глобальном хранилище событий. Эти события представляют собой публичное API вашего контекста. При использовании Event Sourcing вы храните события Event Sourcing в своем локальном хранилище, доступном только для этого ограниченного контекста.
Для каждого потребителя публикуются отдельные события. Необходимо согласовать модели каждого события только с одним потребителем, не требуется определять общую разделяемую модель. DDD называет эти отношения Заказчик / Поставщик (Customer/Supplier).
Возникновение события реального мира “Заказ принят” приводит к публикации отдельных событий для каждого из потребителей: InvoiceOrderAccepted и DeliveryOrderAccepted. Каждое доменное событие содержит только те данные, которые необходимы контексту-получателю.
Я не хочу сейчас обсуждать плюсы и минусы этих подходов. Я хочу просто обратить внимание на то, что можно выбирать количество доменных событий и данные, которые в них хранить.
Это преимущество, которое вы не должны недооценивать, потому что можно решить, как развивать API вашего ограниченного контекста без привязки к событиям Event Sourcing.
Выставление наружу деталей хранения — хорошо известный анти-паттерн. Говоря о хранении, мы в первую очередь думаем о таблицах базы данных, но мы увидели, что события, используемые для Event Sourcing, являются лишь еще одним способом хранения данных. Поэтому выдавать их наружу тоже является анти-паттерном.
Перевод: “хороший разработчик как оборотень — боится серебряных пуль”.
Event Sourcing — это мощный подход, если используется правильно (локально). На первый взгляд кажется, что для событийно-ориентированных архитектур это серебряная пуля, но если присмотреться внимательнее, то видно, что этот подход может привести к сильной связанности … чего вы, конечно, не хотите.
Ответы на вопросы для самопроверки пишите в комментариях, мы проверим, или же задавайте свой вопрос по данной теме.
Комментарии
Оставить комментарий
Разработка программного обеспечения и информационных систем
Термины: Разработка программного обеспечения и информационных систем