Pull to refresh

Comments 39

Вот мне не совсем понятно, как востанавливаются события из снапшотов.
Скажем были у нас следующие события:
create
update name
update password
— snapshot — update password

Если я всё правильно помимаю, то востанавливается объект из событий со времени последнего снапшота.
Т.е. после снапшота объект получает только одно событие update password #2.

Вопрос, что я упускаю в логике работы? Или как мне получить значение Name?
События в снэпшоте не храняться. В нем как раз такие храниться проекциях всех предыдущий событий — состояние агрегата. При обработки новой команды вам нужное получить состояние агрегата, чтобы проверить ваши бизнес правила. Если снэпшотов нету — вы получаете состояние путем проигровывания всех событий данного агрегата (поток событий). Если у вас есть снэпшот, можно сказать что у вас есть промежуточное состояние. Вы восстанавливаете это состояние
_state = snapshot != null ? (UserState) snapshot.Payload : new UserState();

А потом уже сверху накатываете оставшиеся события.
При регенерации все снэпшоты удаляются, так как вы могли изменить класс состояния, и они уже не смогу десериализоваться.
Т.е. снапшоты делаются из агрегатов событий? Верю, работать будет.

Тогда следующий вопрос: А как поиск по этому всему бинарному зоопарку осуществлять? Сериализировать в XML и использовать XPath? А всякие там FullText search с не чётким поиском? А как мне найти пользователей имена которых начинаются на F и входящие группы имена которых начинаются на A или B?

мне это всё нужно будет выгражуть из базы? Проигрывать все события? Или искать только по последним снапшотам, если таковые имеются. А если их нет?
Для всего этого есть read-model.
После того как события сохранились в Event Store они отсылаются на презентационный уровень в Read-model. Концептуально это что-то вроде Views в SQL. Вы обрабатываете там события еще раз и обнавляете read-базу. А в качестве read-базы может быть и SQL и Lucenе и MongoDB, или вообще все вместе.
События сериализуются и сохраняются в append only store (удалять и изменять их мы то не собираемся).
А вот это жесть. Я так понимаю, что если какое-то событие устаревает, то мы должны просто его больше не использовать и создавать новое? Через несколько лет, у нас будет CreateCustomerV200? А база данных это только Key-Value?

Либо я чего-то не понимаю, либо подход весьма узкопрофильный и подходит только для систем построеных на событиях, которые очень и очень редко меняются.
О, придумала. Биржевые брокеры со всякими Форэксами будут счастливы от таких систем. Там одни и те же события уже много и много лет. Банки будут счастливы от такого подхода, там списал деньги — зачислил, упрощаю. Но, что-то я слабо верю в то, что это будет работать с привычными бизнес-сущностями, по которым нужно проводить аналитику, строить отчёты, я уже не говорю о всяких там ОLAP. И кажется это подход, который появился для KeyValue баз данных.
Вы можете добавлять в события поля, просто при десериализации инициализировать их значениями по умолчанию. Переименовывать поля и удалять нельзя (это можно обойти если добавить аттрибуты порядка). Да write-база выглядит как key-value если не смотреть под копот. Но это write-база, больше вам от неё ничего не нужно. Вы даже не сможете прочитать состояние агрегата извне, у него есть только методы изменения состояния. Для отображение есть read-база, там уже может быть что угодно. Можете посмотреть пример к моей предыдущей статье.
Про версии событий детальнее хочется услышать. Чтобы были примеры граблей, на которые можно наступить, если версии не использовать или использовать не верно.
В Lokad, как я вижу, версия используется для снэпшота, чтобы определить к какому снэпшоту применять/восстанавливать события, так?
Но ведь и сами события могут изменятся в процессе разработки, а стек событий уже может быть в базе и при большом восстановлении с нуля, возникает проблема конвертации.

Хочется больше информации на этот счет с примерами. Так как теоретического материала много, но как-то это вскользь все рассматривается.
Без версий ничего не получится, так как важно знать в каком порядке воспроизводить события при восстановлении состояния агрегата. Event Stream — это поток событий для конкретного агрегата. Поток событий характеризуется тем что события с нем упорядочены. Например чтобы восстановить состояние агрегата User, нам нужен поток событий для этого юзера. У потока событий есть ID. Это тот же ID что и у Aggregate Root'a, в данном случае это ID пользователя. То есть получив поток событий с ID = 1 мы сможет по порядку воспроизвести все события которые происходили с пользователем с ID = 1. В итоге получится актуальное состояние пользователя. Если порядок поменять то мы уже не можем гарантировать что получим тоже самое состояние.
В Lokad IDDD Sample снэпшотов нету.
На счет проблем с конвертацие, можете уточнить что именно вы имеете в виду, а то их тут может быть много и в разных местах.
Например в событии было сначала только Name, а потом разделилось на LastName, FirstName. Хотя событие остается одно и то же, пусть будет RenameSomething().
Это ведь real-world case
Самый простой вариант добавить LastName и FirstName, а в коде уже обрабатывать ситуацию когда они не заданы. Но тут тема, конечно, для отдельной статьи. Раньше мы писали патчи, которые проходились по всему event store и меняли устаревшие события на новый формат. Однако, я сейчас склоняюсь к тому, что это не правильных подход.
Возможно будет интересно почитать. MS Patterns & Practices совсем недавно выпустило CQRS Journey — The project is focused on building highly scalable, highly available and maintainable applications with the Command & Query Responsibility Segregation and the Event Sourcing patterns.
Спасибо. Даже не знал что они уже закончили, читал на гитхабе черновики.
Почитал пример на github, сразу же возник вопрос (который собственно в статье и не освещён): у класса UserAR есть только методы по изменению его состояния. Что логично, исходя из всей идеи — объект же будет получать только команды по обновлению своего состояния, а отображаться пользователю данное состояние будет только из read-базы. Но что делать, когда в предметной области необходимо провернуть взаимодействие между двумя объектами? Очевидно, что при таком раскладе чьё-то состояние для начала придётся прочитать. Где его взять то?

Конкретно по 2й части статьи: мне одному кажется, что UserAR, UserState, UserИзReadБазы — это уже немножко много и очень похоже на разработку вокруг конкретной технологии, а никак не вокруг модели (DDD)?
Видите ли, проблема тут в способе хранения данных, как я поняла. То, о чём вы говорите это привычные связи one-to-one, one-to-many, many-to-many. Подобные связи сложно строятся в базах данных key-value based, и там для построения этих связей нужно прилагать не человеческие усилия, я уже не говорю о каскадных обновлениях и удалениях объектов.

Первое, что приходит в голову это реализовывать CompositeCommand которая будет в себе агрегировать команды по обновлению состояний объектов. Но, если сейчас маршрутизация этого всего т.с. direct routing, то для реализации обновлений связей прийдётся реализовывать уже broadcast'ы и всякие там unicast'ы, что в сотни раз усложняет задачу. Для примера, вспомните свои первые приложения на WinAPI где приходилось следить, кто и кому что отправил. Доводилось мне работать в бытности и с Flex'ом, где внутри использовался PureMVC — система страдала как раз от широковещательных отправок.

Более того, даже если и можно было бы пойти этим путём возникает вопрос транзакционности этого всего дела.
Реально, я слабо себе представляю как это всё можно за приемлемое время и деньги разрулить, т.к. в качестве хранения используется MongoDB, то и всякие MSDTC тоже тут не сильно помогут.

Проблем у паттерна очень много, и как я уже говорила это всё подходит только для биржевых систем и других событийно ориентрованых систем, но уж ни как не для CRM'ов, ERP и прочих монстров.
У меня в голове всё же был более простой пример. Ну допустим, корзина в интернет-магазине. Она состоит из позиций (наименование товара, кол-во, сумма). Пользователю нужно показать «Итого». Или сгенерировать правильную сумму для оплаты через платёжную систему. Т.е. для всех добавленных в корзину товаров — получить стоимость каждого, с учётом желаемого количества. Глядя на исходники на github, не представляю как реализовать подобное поведение. Прямо вот напрашивается disclaimer к статье, гласящий о том, что данный подход актуален лишь для некоторого подмножества систем.
Для этого есть специальный термин: Перепроектирование. Если уж так хочется:

public class Order
{
public ICollection Items { get; set;}
public decimal Total
{
get { Items.Sum(i=>i.Price * i.Quantity); }
}
}

Это не совсем по фэншую, но если не стоит задача написания фабрик по выпуску абстрактных фабрик калькуляторов, то это самое оно.
Так ведь, согласно описанию, доступа на чтение к модели у нас нет!
В рамках одного агрегата у вас есть доступ ко всему. Достаточно сделать так чтобы нужные вам сущности оказались в этих рамках.
Так как у вас корее всего будет событие OrderItemAdded, вам достаточно будет написать так
public class OrderState
{
    public ICollection Items { get; private set; }
    public decimal Total { get; private set; }

    public void On(OrderItemAdded e)
    {
        var item = new Item(e.Price, e.Quantity);
        Items.Add(item);
        Total += e.Price + e.Quantity;
    }
}

В read моделе вы тоже можете обработать это событие подобным образом.
А если говорить о заказах и товарном учёте, то используя этот подход с событиями, вы очень много времени потратите на реализацию редактирования приходных и расходных ордеров задним числом. Нет, я всё понимаю, я взрослая девочка, но таковы реалии — очень большое кол-во ПО хотят повторять гибкость Excel'я. Я в бытности знала один банк, который 5 лет вёл ВСЮ калькуляцию в Excel'e, с макросами и прочими плюшками, но факт остаётся фактом.
Если вам надо просто показать «Итого», то тут нету никаких проблем. Вы можете хранить в read модели данные как вам захочется. Можете хранить где-то список позиций для пользователя и каждый раз считать итоговую сумму, или вообще хранить сразу готовую итоговую сумму и обновлять её когда приходит события добавления новой позиции.
Проблемы начнутся когда у вас появится бизнес правило которое зависит от итоговой суммы. Но это задача тоже решается, просто надо правильно спроектировать агрегат, чтобы при добавлении юзером новой позиции у агрегата был доступ к итоговой сумме.
Можете хранить где-то список позиций для пользователя и каждый раз считать итоговую сумму
Вот меня и интересует, где хранить список позиций.
Да, «итого» может быть в read-модели. Но инкрементировать его нельзя, мы же не можем использовать данное значение, находясь в контексте write-модели.
А от вот этого:
public void Mutate(IEvent e)
{
// .NET magic to call one of the 'When' handlers with
// matching signature
((dynamic) this).When((dynamic)e);
}

Мне плакать хочется. Только мы получили строго типизированый язык, как сразу же пошли в обратном направлении. Динамики в первую очередь удобны при работе со всякими там COMами, ActiveXами и прочими IUnknown
Ну это чисто для удобства, чтобы не использовать рефлексию.
Можно обойтись и без динамиков и рефлексии, просто придется явно писать интерфейс для обработки каждого события.
Ну давайте сначала определимся с терминологией. Агрегат — это не одна сущность, это несколько связанных сущностей, к которым мы можем обращаться только через Aggregare Root. Aggregare Root — это корневая сущность агрегата. Когда вы проектируете систему надо как-раз таки выделять агрегаты так, чтобы вам потом было удобно с ними работать в рамках вашей предметной области. Если у вас при изменение какой-то сущности важно знать состояние юзера, то поместите эту сущность в User агрегат. Выглядеть это будет как вложенный объект или коллекция объектов в объекте состояния агрегата. Для того чтобы изменить эту сущность вы будете обязаны обращаться через агрегат, а не на прямую. Нету ни какого смысла выделять по агрегату для каждой сущности из вашей предметной области, это лишь её усложнит. Как раз суть проектирования агрегатов и заключается в том чтобы грамотно скомпоновать сущности.
я подумаю над этим вечером, завтра скажу.
Тут тоже не всё так просто, с подобными графами.
Если у вас при изменение какой-то сущности важно знать состояние юзера, то поместите эту сущность в User агрегат.

В пределе тогда вся логика окажется в классе UserAR :)
Своим вопросом я пытаюсь понять, как предлагается реализовывать непосредственно правила бизнес-логики. Всегда будет необходимость во взаимодействии двух или более объектов. И они легко могут быть из разных агрегатов. Но подход с сокрытием состояния явно мешает подобному взаимодействию. Может просто исходная методология этого не подразумевает на самом деле?
Если у вас все-таки не получилось спроектировать систему так, что любое бизнес правило может быть проверено в рамках одного агрегата, то это проблема, но проблема решаемая.
Я могу вам предложить 3 способа.
1. Самый простой способ — это проверять бизнес правило по данным read модели перед отправкой команды. Но так как в этом случае данные read модели могут быть не консистентны, способ стоит применять только если ваша бизнес модель допускает вероятность ошибки при проверке этого правила, так как на время отправки команды в read модели могут находится не актуальные данные. Вероятность конечно очень мала но она есть. Мы часто используем этот подход так как он самый простой и не накладывает дополнительных ограничений.
2. Вы можете в Command Handler'e восстановить два агрегата, сначала попытаться изменить состояние одного, в котором проверяется бизнес правило. Если не возникло исключительной ситуации, изменить состояние второго агрегата. Но это накладывает ограничение на масштабируемость так как теперь вы не можете разделить эти два агрегата.
3. Самый концептуально правильный подход — это организовать месседжинг между агрегатами. Т.е. вы сначала отправляете команду в один агрегат, там проверяется ваше бизнес правило, инициируется событие, в обработчике этого события в отправляете команду в другой агрегат, которая уже содержит информацию о том что бизнес правило выполнилось.
Что значит если не получилось? Это вполне нормальная ситуация, когда во взаимодействии участвует несколько агрегатов. И я не говорю про бизнес-правила (их проверку организовать относительно несложно). Разговор идёт про получение данных из write-модели в целом и от разных агрегатов в частности.
Ну не получилось например потому, что вы не учли некоторые детали при проектировани и они добавились позже, когда уже нету возможности заново проектировать домен. Это конечно плохо, но всякое случается. В случае с CQRS концептуально неправильно получать данные из write модели. Единственное что вам может вернуть агрегат — это исключение. Конечно вы можете это правило игнорировать, но тогда могут возникнуть проблемы с масштабируемостью. Так же команда должна относить только к одному агрегату, поэтому поднимать какой-либо другой тоже концептуально не правильно, не говоря уже о том чтобы открыть доступ извне к его состоянию.
Это вполне нормальная ситуация с точки зрения DDD. Но CQRS накладывает дополнительные ограничения в угоду масштабируемости.
Вот было бы просто отлично, если бы вы в 1й статье и написали границы применимости и основные ограничения данного подхода. А то те, кто не в курсе — побегут усложнять их корпоративные хелловорлды! :-D
Границ применимости я не вижу, честно говоря, и не думаю что они есть. А ограничений у данного подхода не больше чем у ORM, при том что возможности намного шире.
Постараюсь в следующих статьях это продемонстрировать. Спасибо за ваши комментарии.
Всё же Фаулер и Udi приходят к одинаковому мнению по поводу наличию этих границ. Вчитайтесь!
Они на счет границ ничего не говорят. Говорят только, что в некоторых случаях лучше избегать применения CQRS, чтобы не усложнить систему. У Udi даже пост есть «When to avoid CQRS»
Sign up to leave a comment.

Articles