Да у нас один из сервисов сейчас сделан с Event Sourcing, это было неправильное решение, от него куча проблем.
Возможно неправильным было не решение а реализация.
Так это я хочу убедить вас или хотя бы других, что код в этом случае становится менее поддерживаемым)
Меня вам убедить вряд ли удастся, так как такого подхода я уже наелся достаточно, чтобы понимать его недостатки, мне вас тоже по видимому убедить не получится поэтому в дальнейшей дискуссии я особого смысла не вижу. Если ваш подход работает для вас отлично. Но на вашем месте я не стал бы так категорически утверждать что он лучше других подходов, которые вы не понимаете. На этом уже точно все.
Нет, тут будет именно race condition. Первый процесс создал UpdateRequest, а статус "На проверке" еще не поставил. Второй проверяет статус, он не "На проверке", значит можно создавать UpdateRequest, и тоже создает. С отдельным обработчиком событий, тем более асинхронным, это невозможно предотвратить.
В этом случае нужна компенсирующия операция, если не удалось заблокировать продукт то нужно отменить запрос который инициировал эту блокировку. Ну и запрос нельзя обрабатывать, пока не подтверждена блокировка товара по нему.
А аналитики как должны с базой работать?) Как делать фильтры типа "Показать заявки, где меняется категория"? История событий без финального состояния применима только в очень ограниченных ситуациях
Я не говорил что аналитики должны работать с историей событий непосредственно, я говорил о том что имея полную историю событий агрегата, можно построить любой срез данных, за любой период и презентовать его в любом виде удобном для аналитиков.
Вот поэтому, чтобы сравнивать подходы, и нужен полный код со всеми слоями и несколькими действиями, а не гипотетическое обсуждение.
К сожалению времени на реализацию полного решения у меня нет, да я и не уверен что даже в этом случае вас удастся в чем-то убедить, но если вам интересен пример реализации чего то посложнее то рекомендую это: https://www.microsoft.com/en-us/download/details.aspx?id=34774
Тогда это будут просто сеттеры, и можно будет привести Product в любое состояние без соблюдения инвариантов, чего с логикой в сущностях мы хотим избежать
Нет, в этих методах мы можем делать проверку assertIsEditable() и бросать исключение если редактирование продукта заблокировано.
Товар не может совсем не знать, у него есть статус "На проверке",
Я бы вместо дополнительного статуса просто добавил бы свойство isEditable
Значит в Product как минимум должен быть метод, который его устанавливает.
Я думаю что как реакцию на UpdateRequestApproved можно выполнять Product::allowEdit, а потом changeName и все остальное.
Из UpdateRequest мы его вызывать не можем, потому что это другой агрегат, значит надо делать доменный сервис
Но можем как реакцию на событие UdpateRequestApproved
Замена доменного сервиса на обработчик событий ничего не меняет, а также создает состояние гонки, когда UpdateRequest уже создан, а статус в Product еще не поставлен.
Я бы сказал создает eventual consistency, но насколько я понял это и так происходит, если заявка на изменение происходит в другой системе.
Статусы Created и Sent для него имели бы специальные значения, при открытии страницы просмотра товара надо было бы искать в истории последнюю запись для этого товара в одном из этих статусов, чтобы наложить изменения, это создает лишнюю нагрузку на базу.
Можно повесить обработчики событий которые создадут специальную проекцию (денормализированную таблицу в базе) специально заточенную под операции чтения для показа истории.
Для обработки UpdateRequest удобнее видеть diff, а для просмотра товара удобнее финальные значения.
Данные для чтения не нужно хранить в агрегате, когда есть полная история событий, которые публикуют агрегаты, на их базе можно строить разные проекции специально оптимзированные под разные UI. Это одно из преимуществ CQRS.
как без данных из Product проверить ограничение "Для отправки на проверку должно быть описание не менее 300 символов, и хотя бы 1 изображение".
Если я правильно понял через апдейт реквесты редактировать должны только поставщики, сотрудники магазина могут редактировать напрямую независимо от того есть апдейт реквест или нет
А зачем специальный метод? Разве нельзя прооапдейтить продукт штатными методами? changeName, changeDescription и т.д. Допустим поставщик захотел изменить описание продукта и категорию. Он создаёт updateReqest с этими изменениями, менеджер его апрувит, публикуется событие updateReqestApproved которое слушает подписчик, который апдейтит товар. Товар тогда вообще ничего не знает о этих , апдейт реквкестах.
В теории ProductUpdateRequest будет похож на ProductCreationRequest, и после его апрува нужно будет только проапдейтить продукт данными из заявки, и вся логика тоже будет за пределами продукта.
Создание товара я не делал, так как с моей точки зрения оно находится вне контекста запроса на добавление или изменение товара, а происходит как реакция на событие ProductCreationRequestApprovedEvent его слушает подписчик, который получает из заявки информацию о товаре, и запускает команду создания товара, передавая данные из заявки. В требованиях которые вы описали я не увидел никакой логики которую можно было бы поместить в агрегат Товар.
Хотя я бы все таки сделал два отдельных класса так как в заявке на добавление все поля по идее должны быть обязательные, а на редактирование нет, там нужно указывать только те поля которые поставщик хочет изменить.
Помимо того что сущность жестко связана со слоем инфраструктуры и презентации мне не совсем понятно зачем всю логику помещать в один класс Product. Я бы реализовал это так: - ProdcutCreationRequest - когда поставщик добавляет запрос не добавление нового продукта в систему. Этот Request может быть одобрен, и тогда в системе создается новый продукт, с данными из реквеста, или отклонен, и тогда поставщик создает новый реквест на базе старого с исправлениями. - ProductChangeRequest - если поставщик правит данные по существующему товару. Тут логика похожая, если реквест утвержден, то изменения применяются к товару, если нет то поставщик может создать новый реквест на базе предыдущего.
Так как логика этих двух классов схожа можно в принципе их объединить в один класс с указанием типа, productCreation или productChange, либо поместить общую логику в абстрактный класс.
Таким образом сами эти реквесты будут содержать в себе историю изменений. И эта логика не будет засорять логику самого товара.
Ясно, если для вас все это является критическими недостатками, не буду спорить, используйте свой подход. Как я уже писал раннее, если ваш подход позволяет вам строить качественные, надежные, легко поддерживаемые приложения, это очень здорово, но возможно вы еще не сталкивались с проблемами которые решает DDD чтобы понять преимущества этого подхода. Больше мне добавить нечего.
Ничего не мешает, это то же самое. Вместо свойства Transfer::amount у нас теперь OverdraftUsedEvent::amount, поэтому по "Find Usages" места использования второго свойства показаны не будут.
Не понял, почему find usages не покажет вам OverdraftUsedEvent::amount? И это не тоже самое, потому что в данном случае типизация есть, а вы это указали недостатком событийного подхода.
Кроме того, при асинхронной обработке это DTO все равно будет сериализовано, и чтобы отследить места использования, надо гарантировать, что в коде обработчика оно будет десериализовано в этот же класс, что далеко не всегда происходит.
Что мешает использовать ту же библиотеку для десериализации в обработчике?
Это не отправка сообщений в очереди, а прямые вызовы API, но ладно, это неважно.
А в чем принципиальная разница?
// а транзакция после вызова этого метода не закоммитилась
Событие не сохранится в базу данных вместе с агрегатом. Более подробно читайте в описании шаблона Transactional Outbox.
это может произойти в любом подходе.
не в любом, подход который я описал выше как раз позволяет этого избежать, и прекрасно сочетается с событийной архитектурой.
Я не писал что уведомление нужно слать как реакцию на исключение
Хотя нет, писал, должен признать, что пример был неудачный, вы правильно заметили что в данном случае лучше пробросить even bus в агрегат и отправить сообщение.
Ну то есть в одном случае отправляем уведомление из вызывающего кода по эксепшену, в другом из самого агрегата через событие.
Я не писал что уведомление нужно слать как реакцию на исключение, я писал что исключение может обрабатывается несколькими уровнями выше. Если нужна коммуникация с внешней системой то в данном случае перед тем как бросить исключение нужно также выслать событие. Да, это немного спорный вопрос так как состояние агрегата не изменилось, но я пока не нашел другого способа в ситуации когда нужно и отобразить ошибку, и отреагировать на нее асинхронно.
Да, если есть возможность, то я бы делал вызовы явно
Т.е. пользователь при переводе денег будет ждать пока ваш сервис отправит все запросы к внешним API, а если одно из них в данный момент не работает то перевод вообще не удастся?
Или еще лучше, при овердрафте надо разрешать действие, но отправлять уведомление.
Отправлю из агрегата событий OverdraftUsed. На это событие может реагировать не только система уведомлений но множество других систем, например модуль статистики который накапливает информацию о том как часто пользователь пользуется овердрафтом. По вашему мы должны добавлять вызов каждой из этих систем из сервиса который управляет агрегатом?
Это справедливо если используется подход Anemic Model. В Rich Model вся бизнес логика находится внутри доменных объектов.
Возможно неправильным было не решение а реализация.
Меня вам убедить вряд ли удастся, так как такого подхода я уже наелся достаточно, чтобы понимать его недостатки, мне вас тоже по видимому убедить не получится поэтому в дальнейшей дискуссии я особого смысла не вижу. Если ваш подход работает для вас отлично. Но на вашем месте я не стал бы так категорически утверждать что он лучше других подходов, которые вы не понимаете. На этом уже точно все.
В этом случае нужна компенсирующия операция, если не удалось заблокировать продукт то нужно отменить запрос который инициировал эту блокировку. Ну и запрос нельзя обрабатывать, пока не подтверждена блокировка товара по нему.
Я не говорил что аналитики должны работать с историей событий непосредственно, я говорил о том что имея полную историю событий агрегата, можно построить любой срез данных, за любой период и презентовать его в любом виде удобном для аналитиков.
К сожалению времени на реализацию полного решения у меня нет, да я и не уверен что даже в этом случае вас удастся в чем-то убедить, но если вам интересен пример реализации чего то посложнее то рекомендую это: https://www.microsoft.com/en-us/download/details.aspx?id=34774
Нет, в этих методах мы можем делать проверку
assertIsEditable()
и бросать исключение если редактирование продукта заблокировано.Я бы вместо дополнительного статуса просто добавил бы свойство isEditable
Я думаю что как реакцию на UpdateRequestApproved можно выполнять Product::allowEdit, а потом changeName и все остальное.
Но можем как реакцию на событие UdpateRequestApproved
Я бы сказал создает eventual consistency, но насколько я понял это и так происходит, если заявка на изменение происходит в другой системе.
Можно повесить обработчики событий которые создадут специальную проекцию (денормализированную таблицу в базе) специально заточенную под операции чтения для показа истории.
Данные для чтения не нужно хранить в агрегате, когда есть полная история событий, которые публикуют агрегаты, на их базе можно строить разные проекции специально оптимзированные под разные UI. Это одно из преимуществ CQRS.
Получить эти данные из продукта через outside.
Если я правильно понял через апдейт реквесты редактировать должны только поставщики, сотрудники магазина могут редактировать напрямую независимо от того есть апдейт реквест или нет
Пока товар на проверке его вообще никто не может редактировать, даже менеджер?
И почему UpdateRequest не может хранить значения полей товара, которые надо изменить?
А зачем специальный метод? Разве нельзя прооапдейтить продукт штатными методами? changeName, changeDescription и т.д. Допустим поставщик захотел изменить описание продукта и категорию. Он создаёт updateReqest с этими изменениями, менеджер его апрувит, публикуется событие updateReqestApproved которое слушает подписчик, который апдейтит товар. Товар тогда вообще ничего не знает о этих , апдейт реквкестах.
В теории ProductUpdateRequest будет похож на ProductCreationRequest, и после его апрува нужно будет только проапдейтить продукт данными из заявки, и вся логика тоже будет за пределами продукта.
Создание товара я не делал, так как с моей точки зрения оно находится вне контекста запроса на добавление или изменение товара, а происходит как реакция на событие
ProductCreationRequestApprovedEvent
его слушает подписчик, который получает из заявки информацию о товаре, и запускает команду создания товара, передавая данные из заявки. В требованиях которые вы описали я не увидел никакой логики которую можно было бы поместить в агрегат Товар.Примерно так бы выглядел доменный слой заявки на добавление продукта
https://gitlab.com/grifix/grifix/-/tree/feature/habr/modules/sandbox/src/Domain
и тесты к нему
https://gitlab.com/grifix/grifix/-/tree/feature/habr/modules/sandbox/tests/Unit/Domain/ProductCreationRequest
Хотя я бы все таки сделал два отдельных класса так как в заявке на добавление все поля по идее должны быть обязательные, а на редактирование нет, там нужно указывать только те поля которые поставщик хочет изменить.
Помимо того что сущность жестко связана со слоем инфраструктуры и презентации мне не совсем понятно зачем всю логику помещать в один класс Product. Я бы реализовал это так:
- ProdcutCreationRequest - когда поставщик добавляет запрос не добавление нового продукта в систему. Этот Request может быть одобрен, и тогда в системе создается новый продукт, с данными из реквеста, или отклонен, и тогда поставщик создает новый реквест на базе старого с исправлениями.
- ProductChangeRequest - если поставщик правит данные по существующему товару. Тут логика похожая, если реквест утвержден, то изменения применяются к товару, если нет то поставщик может создать новый реквест на базе предыдущего.
Так как логика этих двух классов схожа можно в принципе их объединить в один класс с указанием типа, productCreation или productChange, либо поместить общую логику в абстрактный класс.
Таким образом сами эти реквесты будут содержать в себе историю изменений. И эта логика не будет засорять логику самого товара.
Ясно, если для вас все это является критическими недостатками, не буду спорить, используйте свой подход. Как я уже писал раннее, если ваш подход позволяет вам строить качественные, надежные, легко поддерживаемые приложения, это очень здорово, но возможно вы еще не сталкивались с проблемами которые решает DDD чтобы понять преимущества этого подхода. Больше мне добавить нечего.
Не понял, почему find usages не покажет вам OverdraftUsedEvent::amount?
И это не тоже самое, потому что в данном случае типизация есть, а вы это указали недостатком событийного подхода.
Что мешает использовать ту же библиотеку для десериализации в обработчике?
А в чем принципиальная разница?
Событие не сохранится в базу данных вместе с агрегатом. Более подробно читайте в описании шаблона Transactional Outbox.
не в любом, подход который я описал выше как раз позволяет этого избежать, и прекрасно сочетается с событийной архитектурой.
Разница есть и она довольно существенная.
То что так часто делают не означает что это правильно.
Что произойдет в этом случае?
Что мешает сделать так:
Хотя нет, писал, должен признать, что пример был неудачный, вы правильно заметили что в данном случае лучше пробросить even bus в агрегат и отправить сообщение.
Я не писал что уведомление нужно слать как реакцию на исключение, я писал что исключение может обрабатывается несколькими уровнями выше. Если нужна коммуникация с внешней системой то в данном случае перед тем как бросить исключение нужно также выслать событие. Да, это немного спорный вопрос так как состояние агрегата не изменилось, но я пока не нашел другого способа в ситуации когда нужно и отобразить ошибку, и отреагировать на нее асинхронно.
Т.е. пользователь при переводе денег будет ждать пока ваш сервис отправит все запросы к внешним API, а если одно из них в данный момент не работает то перевод вообще не удастся?
Это почему-же?
Отправлю из агрегата событий OverdraftUsed. На это событие может реагировать не только система уведомлений но множество других систем, например модуль статистики который накапливает информацию о том как часто пользователь пользуется овердрафтом. По вашему мы должны добавлять вызов каждой из этих систем из сервиса который управляет агрегатом?