Как стать автором
Обновить

Комментарии 48

А представьте, что к вам приходит бизнес и говорит, что Id у объекта Order должен быть int и определяться уже после запроса в БД, а вам нужно писать событие OrderCreatedEvent с указанием id созданного заказа. Плюс к тому же данные транзакции и событие должны писаться в рамках одной транзакции (outbox) и событие изменения должно публиковаться как интеграционный эвент вместо доменного. Плюс к тому же это интеграционное событие должно быть ответом на другое событие из какого-нибудь CorrelationId и указанием конкретной очереди, куда нужно писать и передачей некоторых параметров из прошлого события (например, того же CorrelationId), но указывать его надо не в тело сообщения, а в headers.

Вот тогда ваш подход начнет ломаться.

А представьте, что к вам приходит бизнес и говорит, что Id у объекта Order должен быть int и определяться уже после запроса в БД, а вам нужно писать событие OrderCreatedEvent с указанием id созданного заказа.

Верное замечание, на самом деле, но поэтому я и привел несколько способ реализации этого шаблона. В данном случае мы можем воспользоваться 1 способом с прямой публикацией из сервиса, то есть мы сначала сохраняем заказ в БД, получая его Id (типа int), затем публикуем событие о создании во внутреннем издателе.

Плюс к тому же данные транзакции и событие должны писаться в рамках одной транзакции (outbox)

Мы можем вызывать метод HandleEvents() в рамках одной транзакции с основной бизнес-логикой - это больше зависит от требований, но проблем никаких нет.

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

Можно создать обработчик этого события, который будет отправлять это событие куда угодно: сохранять в базе, класть в очередь сообщений для других сервисов, либо отправлять в синхронном стиле по HTTP/REST или gRPC - здесь на ваше усмотрение. Честно говоря, не совсем понял смысл фразы "интеграционный эвент вместо доменного", мы можем создавать интеграционное событие в ходе обработки доменного.

Плюс к тому же это интеграционное событие должно быть ответом на другое событие из какого-нибудь CorrelationId и указанием конкретной очереди.

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

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

Повторюсь, но в обработках событий мы можете делать вообще что хотите, хоть просто в консоль писать информацию о событии. Собственно, вы точно также можете писать в headers требуемый CorrelationId.

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

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

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

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

Посмотрел подробнее - хендлеры доменных ивентов вызываются в DomainEventPublisher из RequestResponseLoggingMiddleware уже после коммита транзакции. Так что предложенная реализация не подходит ни для доменных ивентов, так как результаты нельзя сохранить в одной транзакции, ни для интеграционных ивентов, так как нельзя сделать Outbox и обеспечить at-least-once гарантию доставки.

Можно встроить фиксацию транзакции после обработки событий. Для этого Unit of Work обычно используется.

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

Судя по тому что пишут в литературе доменное событие - это то что происходит внутри приложения, а интеграционное событие пересылается между разными приложениями.

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

Каждый микросервис - это отдельное приложения и между ними как раз идёт обмен интеграционными событиями. Слабо себе представляю, что внутри микросервиса используются доменные события. Хотя такое полностью исключить нельзя.

https://learn.microsoft.com/ru-ru/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation:

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

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

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

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

Но так же согласен с @KrawMire, что есть вопрос - что считать процессом. Например, в монолите при хорошей модульности (изоляции модулей) можно считать интеграционными событиями те события, на которые могут подписаться модули, не бросавшие это событие.

А еще хендлер доменного ивента может изменить какой-то агрегат, который тоже захочет отправить об этом ивент. Как решить этот кейс?

Можно реализовать набор событий в издателе не в виде коллекции, а в виде очереди, причем потокобезопасную (чтобы исключения не выбрасывались при добавлении новых событий в коллекцию во время перебора в цикле), либо сделать перебор не через foreach, а в цикле while для перебора до конца.

Таким образом, когда мы будем изменять агрегат и он будет создавать новое событие, то оно будет без проблем добавляться и затем обрабатываться даже в процессе обработки другого события.

"Настоящий" DDD - он как настоящий шотландец. Или как подростковый секс - все о нём говорят, но мало кто практикует.

А если серьёзно, то чем больше я смотрю на тактический ДДД, тем больше понимаю, что это попытка сделать модуль внутри приложения и закрыть его контрактом. Наверное в больших проектах от этого будет профит, т.к. мы уменьшаем вероятность что-то сломать, но в небольших проектах или для простых сущностей - привнесённая сложность перевешивает профит.

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

Согласен. По сути, так и есть. Основная идея DDD в том, чтобы инкапсулировать модель предметной области в некий модуль, чтобы все инфраструктурные детали разрабатывать независимо от бизнес-логики. И да, вы правы, что польза от такого подхода появляется только в ходе использования в больших проектах, а в мелких - скорее усложнит систему и не окупит затраченных усилий.

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

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

Как мы видим, наш сервис несет ответственность буквально за все действия, что очень сильно усложняет внесение изменений

Он несет ответственность, только если все действия реализованы в нем. Если он просто вызывает другие компоненты в нужном порядке, то никакой проблемы в этом нет, он для этого и нужен.

OrderItems = newOrder.Items

Если вы передаете в событие сущность OrderItem, то можно и Order так же передавать, необязательно копировать все свойства Order, да еще и с другими именами.

ClientPhoneNumber = request.PhoneNumber

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

В рамках нашего примера, мы добавим новый фабричный метод в сущность Order , который будет отвечать за создание нового экземпляра заказа

То есть заказ будет создавать сам себя? Это не выглядит похожим на домен.
Если мы переместим статический метод CreateNewOrder в другой класс, то он никак не изменится. А если так, то зачем его надо было туда помещать? Смысл методов класса в том, что они могут работать с его внутренним состоянием, чтобы вызывающему коду не надо было работать с деталями реализации. А статические методы могут работать только со статическими переменными, что фактически формирует отдельный класс. А если нам все равно нужен отдельный класс, то лучше сделать его явно.

Id = Guid.NewGuid(),
CreatedAt = DateTime.Now,

Зависимости в виде глобальных переменных. Что если нам нужен специальный генератор id, передавать технический компонент в аргументах вместе с бизнес-данными? Видите, как смешиваются разные уровни абстракции?

return (order, createEvent);

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

Мы снова уменьшили ответственность нашего сервиса

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

но теперь нам также необходимо передать издателя событий в качестве аргумента в этот метод

Что насчет логгера, тоже будете передавать аргументом? А другие зависимости?
Вот так постепенно ваш метод превращается в сервис.

public static CreateNewOrder()
publisher.AddEvent(createEvent);

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

// Создаем свойство только для чтения наших событий
public IReadOnlyCollection Events => _events.AsReadOnly();

О каких чистых сущностях можно говорить, если у нас там есть публично доступные технические свойства. Это же domain-driven design, у вас в domain у этой сущности разве есть свойство Events?

Теперь наша сущность стала сама по себе хранилищем для событий.

Что там, говорите, было с ответственностями у сервиса?) Зачем мы его начали переделывать?

Таким образом, мы полностью избавили наш сервис от необходимости знать про какие-либо периферийные детали обработки событий.

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

Он несет ответственность, только если все действия реализованы в нем. Если он просто вызывает другие компоненты в нужном порядке, то никакой проблемы в этом нет, он для этого и нужен.

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

Если вы передаете в событие сущность OrderItem, то можно и Order так же передавать, необязательно копировать все свойства Order, да еще и с другими именами.

Да, согласен, некоторые части примеров неидеальны, но на них и не ставится акцент. Основная задача - показать способы реализации и использования событий предметной области.

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

Здесь то же самое - цель в статье стояла другая. Это моя ошибка, согласен, что не продумал до конца рассматриваемую систему. Я ставил в приоритет именно реализацию шаблона.

То есть заказ будет создавать сам себя? Это не выглядит похожим на домен.

На самом деле, это достаточно распространенный шаблон для сущностей в DDD, когда нам нужно изолировать логику валидации входных параметров при создании сущности, в то время как конструктор нужен только для ORM. Соглашусь с тем, что вариант с реализацией фабричного метода в родительском классе выглядит лучше, но в данном случае заказ является агрегатом.

Что если нам нужен специальный генератор id, передавать технический компонент в аргументах вместе с бизнес-данными?

Я указал на это в самой статье.

Вот-вот, надо еще и сложное состояние возвращать. 

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

Этот сервис и был нужен для содержания этой ответственности.

Как сказал выше, зависит от решений в проекте. Но в идеале, сервис не должен содержать логики по отправке уведомлений и прочим подобным действиям. Это не его ответственность.

Что насчет логгера, тоже будете передавать аргументом?

Смотря о логировании чего вы говорите. Логирование событий может делать издатель.

Оплата не прошла, заказа в базе нет, а другие системы получат уведомление и сохранят информацию о заказе.

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

Это же domain-driven design, у вас в domain у этой сущности разве есть свойство Events?

Да, крайне распространенный подход. Microsoft, например, об этом прямо пишет в своей документации.

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

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

Но даже если обращаться к тому же SOLID, то это явное нарушение принципа единой ответственности

Нет, принцип единой ответственности не запрещает передавать в конструктор 2 зависимости и вызывать из них по 1 методу.

так как при изменении процессов, нам надо будет всегда изменять метод сервиса

Ну так это и правильно, потому что сервис это модель этих процессов. Поменялись процессы - поменялась модель.

Сервис в идеале должен исполнять то, что относиться непосредственно к логике системы.

Последовательность "Сначала сохранить, потом отправить в другую систему" это и есть логика, она обсуждается с бизнесом.

На самом деле, это достаточно распространенный шаблон для сущностей в DDD

Если что-то распространено, это не значит, что это правильно. Я привел конкретные недостатки, они не исчезают от того, что многие так делают.

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

Не знаю, что вы подразумеваете под родительским классом, я имел в виду, что это метод должен оставаться в сервисе. Потому что так получается меньше недостатков.

Точно также, про это было сказано в самом материале.

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

Но в идеале, сервис не должен содержать логики по отправке уведомлений и прочим подобным действиям.

Если у вас в бизнес-требованиях написано "Сохранить заказ, потом отправить уведомление", то должен быть код, который выполняет оба действия в одном методе, это и есть сервис, и выполнение их в нужном порядке это и есть его ответственность.
А по вашей логике получается, что и события отправлять нельзя, так как это другая ответственность.

Обработка событий происходит после окончания выполнения метода.

То есть _publisher.AddEvent(event) ничего никуда не отправляет, а просто имитирует, и у нас где-то есть еще код, который выполняется после логики и делает фактическую отправку? А потом у программистов отладка занимает по 2 часа в каждой задаче)

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

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

Да, крайне распространенный подход. Microsoft, например, об этом прямо пишет в своей документации.

Ну и что, что он распространенный? Несоответствие заявленным целям от этого не исчезает.

мы идем в сущность
идем в сервис

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

Ну так это и правильно, потому что сервис это модель этих процессов. Поменялись процессы - поменялась модель.

При работе в команде в итоге это может привести к конфликтам, если два человека будут изменять один и тот же метод в рамках разных задач. Плюс, не всегда изменения будут касаться "только одного вызова", это зачастую будет требовать добавления дополнительной логики.

Последовательность "Сначала сохранить, потом отправить в другую систему" это и есть логика, она обсуждается с бизнесом.

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

Если что-то распространено, это не значит, что это правильно. Я привел конкретные недостатки, они не исчезают от того, что многие так делают.

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

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

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

Если у вас в бизнес-требованиях написано "Сохранить заказ, потом отправить уведомление", то должен быть код, который выполняет оба действия

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

То есть _publisher.AddEvent(event) ничего никуда не отправляет, а просто имитирует, и у нас где-то есть еще код, который выполняется после логики и делает фактическую отправку?

Об этом написано в статье в "Подготовка. Создания издателя и обработчиков событий", а сам вызов обработки событий в "Способ 1. Публикация напрямую из сервиса". Да, события не выполняются сразу после публикации. И причем здесь про отладку, не совсем понимаю, вы транзакциями при работе с БД тоже управляете в каждом методе отдельно? Обычно, это общая логика, которая помещается, например, в middleware.

Если в каких-то обработчиках происходит фактическая отправка событий в другую систему, то не позволит. Условная Кафка не связана с транзакциями базы.

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

Ну и что, что он распространенный? Несоответствие заявленным целям от этого не исчезает.

Хранение событий в сущности - это также может являться частью модели предметной области. Не понимаю вопроса в этом плане.

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

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

При работе в команде в итоге это может привести к конфликтам

Конфликты могут быть всегда, это не слишком большая проблема. Обычно можно сделать такие задачи последовательно, а не параллельно.

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

У бизнес-логики есть общепринятое определение.

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

Реализация бизнес-требования "Если все части задания выполнены, отправить письмо руководителю" это бизнес-логика.
Как вы устанавливаете значения в this это не бизнес-логика, а детали реализации.

Нам может потребоваться создавать заказ и в других методах этого же сервиса, либо вообще в других частях системы.

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

И причем здесь про отладку, не совсем понимаю

Потому что отложенным выполнением нельзя пройти логику по шагам в отладчике и проверить, что не работает вот этот обработчик событий создания заказа. Надо заранее знать, какой именно обработчик работает неправильно, и ставить туда брейкпойнт. Или поставить во все, а их сначала надо найти. А может он не работает потому что данные в событии неправильно формируются. Тогда надо возвращаться обратно, перезапускать, еще раз смотреть. С явными синхронными вызовами это всё обычно проще.

вы транзакциями при работе с БД тоже управляете в каждом методе отдельно?

Да, потому что иногда бывает так, что надо сделать 2 транзакции. Сначала сохраняем заказ с одним статусом, потом производим оплату, потом сохраняем с другим. Иногда надо получить данные из другой системы, и держать транзакцию открытой всё действие неправильно. Иногда транзакциями управляет ORM, но там все равно надо вызывать какой-нибудь метод flush() для фактического сохранения.

Причем здесь Кафка, если речь про внутренние события?

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

Не понимаю вопроса в этом плане.

DDD говорит про чистые сущности, но не может это обеспечить.

У бизнес-логики есть общепринятое определение.

Я говорю про соглашения, принятые на проекте. Разные команды могут по-разному понимать какие-либо детали реализации. Я рассматривал в своем примере только логику, относящуюся к внутреннему состоянию системы.

Правила валидации обычно зависят от действия, а не от сущности.

Правила валидации входных DTO - да. Правила валидации сущностей чаще всего одни на всех - это же инвариант самой сущности.

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

Частично можно. Middleware -> Вызов метода сервиса -> Вызов обработки событий.
Но на самом деле, здесь вы правы. Сам факт применения MediatR усложняет этот процесс, так как он вызывает обработчики "под капотом". Но, опять же, решения для всего и вся не существует и часто приходиться идти на компромиссы.

Да, потому что иногда бывает так, что надо сделать 2 транзакции.

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

Потому что нужно отправить данные созданной сущности в другую систему.

Не, я про то, что данные не отправятся, если основная транзакция завершится неудачей. Так что обработчик не вызовется.

DDD говорит про чистые сущности, но не может это обеспечить.

Да, пока что универсального подхода, который был бы хорош во всем, к сожалению, не придумали. Я в статье старался рассказать просто про один интересный подход к разбиению логики, без сильной привязки к DDD, но без него никуда в рамках моего рассуждения :(

Я говорю про соглашения, принятые на проекте.

Если вы под бизнес-логикой понимаете что-то другое, то у вас и будет не DDD, а какой-то свой подход.

Правила валидации сущностей чаще всего одни на всех - это же инвариант самой сущности.

Допустим, у какой-то сущности есть статус "На модерации". В это время пользователь не может ее редактировать. При попытке изменить любые данные API должно возвращать ошибку "Сущность находится на модерации" (допустим у нас сложный агрегат, и есть разные методы редактирования). Но прохождение модерации означает изменение статуса, а это тоже редактирование, но его делать можно. При этом самому пользователю не должно быть разрешено менять этот статус. Тут нужные проверки зависят от действия.

Потому что нужно отправить данные созданной сущности в другую систему.

У меня тут потерялось слово "иногда". Но согласен, так будет работать, я сначала неправильно понял вашу мысль.

Да, пока что универсального подхода, который был бы хорош во всем, к сожалению, не придумали.

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

Если вы под бизнес-логикой понимаете что-то другое, то у вас и будет не DDD, а какой-то свой подход.

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

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

Да, сущность сама регулирует, что когда она "на модерации", то её поля не могут измениться. Процесс же изменения статуса действительно зависит от инфраструктурных деталей (проверка роли пользователя/модератора). То есть пока мы не поменяем статус с "на модерации" на какой-то другой, то сущность сама не будет давать нам изменять поля. Статус же чаще всего меняться через специальный метод для этого.

Логика в сервисах это вполне нормальное решение.

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

Но конечной целью, опять же, было просто показать несколько способов реализации публикации и обработки событий. Просто когда я изучал эту тему, то приходилось собирать по кускам информацию, так как даже в книгах эта тема реализовывалась очень странно (синглтон издатель на всё приложение). И вот, я решил собрать все более-менее адекватные варианты реализации в одной статье.

Мы просто с вами тут уже фундаментальные вопросы к DDD рассматриваем :)

Абсолютно точно, анемичная модель - это вполне нормальное решение.

Мне кажется, что вы рано сдались в споре. Можно было еще упомянуть, что анемичные модели достаточно давно уже считаются антипаттерном и нет пока ни одного подхода, который бы нивелировал негативные последствия несоблюдения инкапсуляции доменных моделей. Rich Model в свою очередь имеет только один недостаток - часто Aggregate Root обрастает слишком уж большим количеством методов. С такой проблемой я и мои сотрудники жить готовы, так как альтернатива страшнее.

анемичные модели достаточно давно уже считаются антипаттерном

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

который бы нивелировал негативные последствия несоблюдения инкапсуляции

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

Представьте, что все значения свойств сущности хранятся в одном поле data в виде JSON. Это детали реализации. Как будет выглядеть ваша бизнес-логика внутри сущности с обращениями к такому полю?

Rich Model в свою очередь имеет только один недостаток

В Rich Model гораздо больше недостатков, просто вы их не замечаете по вышеуказанной причине. От смешивания разных уровней абстракции до сложностей с пробросом зависимостей внутрь и ошибок валидации наружу для показа в интерфейсе.

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

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

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

В Rich Model гораздо больше недостатков, просто вы их не замечаете по вышеуказанной причине. От смешивания разных уровней абстракции до сложностей с пробросом зависимостей внутрь и ошибок валидации наружу для показа в интерфейсе.

Про смешивания уровней абстракции - не смешивайте и не будет таких недостатков. Доменный слой работает только с инструментами доменного слоя. Под капотом реализация этих инструментов может быть в слое инфраструктуры, но это implementation details.

Сложность с пробросом зависимостей в некоторых проектах действительно есть, но в своих проектах я эту сложность преодолел выиграв при этом в производительности.

Ошибки домена не связаны с интерфейсом. Наверх вы можете пробросить исключение, произошедшее в домене. Такое исключение должно отловиться в application слое, который уже решит какое исключение application слоя бросить дальше. Но скорее вы говорите про валидацию форм - тут вообще все еще на уровне контроллера с валидаторами в большинстве случаев должно решиться. Для более специфичных случаев должны быть и более специфичные решения, которые будут специфичными как с анемичными моделями, так и с rich моделями.

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

Если вы позволяете создаться сущности в невалидном состоянии, то о какой инкапсуляции и Rich моделях может идти речь?

Представьте, что все значения свойств сущности хранятся в одном поле data в виде JSON. Это детали реализации. Как будет выглядеть ваша бизнес-логика внутри сущности с обращениями к такому полю?

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

Инкапсуляция - это процесс соблюдения инвариантов объекта самим объектом.

Нет, у этого термина другое общепринятое определение.

Encapsulation refers to the bundling of data with the mechanisms or methods that operate on the data. It may also refer to the limiting of direct access to some of that data, such as an object's components. Essentially, encapsulation prevents external code from being concerned with the internal workings of an object.

"Limiting of direct access" может включать в себя соблюдение инвариантов, а может и не включать.

И я не пойму почему вообще речь пошла про свойства?

Потому что под инкапсуляцией обычно понимают сокрытие деталей реализации от внешнего кода, в том числе свойств. Хотя изначально он означал просто помещение методов и свойств в один класс (in capsula).

Также понятие инвариант, про которое вы говорите, относится к значениям свойств.

Про смешивания уровней абстракции - не смешивайте и не будет таких недостатков.

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

Ошибки домена не связаны с интерфейсом.

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

Наверх вы можете пробросить исключение, произошедшее в домене.

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

Если вы позволяете создаться сущности в невалидном состоянии, то о какой инкапсуляции и Rich моделях может идти речь?

Ни о какой, о том и речь, что даже с Rich Model это недостижимо.
Про сервисы тоже можно так сказать - зачем вы в сервисах позволяете создаться сущности в невалидном состоянии, можно же следить и создавать в валидном.

- Представьте, что все значения свойств сущности хранятся в одном поле data в виде JSON.
- какая разница как хранится информация в БД или в файлах?

При чем тут БД или файлы, если я про них ничего не говорил?
"Поле data" означает поле data в классе сущности.

class Entity {
  private array $data;

  public function someLogic(float $value)
  {
    $this->data['someProperty'] = $this->data['someProperty'] + $value;
  }
}

Вот захотел я так хранить данные в сущности, и храню. Это детали реализации. С вашим подходом детали реализации протекают в бизнес-логику.

Сколько лет уже светлые умы пытаются отучить людей от того, что инкапсуляция про сокрытие, а никак не получается. Даже на Wiki написано (https://ru.wikipedia.org/wiki/Инкапсуляция_(программирование))

Целью инкапсуляции является обеспечение согласованности внутреннего состояния объекта.

Более техническое определение: инкапсуляция - процесс соблюдения инвариантов. Что по сути одно и тоже, но более привычно уху программиста.
Есть и куча других мест, где достаточно авторитетно это заявляется. Сокрытие внутренней сложности - это побочный, но приятный бонус, дающий возможность не замусоривать публичный API объекта и экономить когнитивный ресурс программиста. Давайте договоримся, что вы усвоите эту информацию прежде чем спорить о чем-то еще.

пытаются отучить людей от того, что инкапсуляция про сокрытие

Я в курсе, что это не сокрытие. Но вы используете этот термин явно не в варианте "помещение методов в один класс с данными", так как нарушение этого правила само по себе ни на что не влияет. Можно сделать класс с публичными свойствами и поместить туда несколько методов, это будет инкапсуляция, но если вы их решите вынести из класса (т.е. нарушить это правило), то новых проблем с соблюдением инвариантов это не создаст. В Python есть инкапсуляция, но к любому свойству объекта можно обратиться извне, поэтому гарантии инвариантов нет.

Тем не менее, по ссылке, которую я привел, написано "Under the definition that encapsulation "can be used to hide data members and member functions", the internal representation of an object is generally hidden outside of the object's definition.".

Даже на Wiki написано
Давайте договоримся, что вы усвоите эту информацию прежде чем спорить о чем-то еще.

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

Да честно говоря, я просто устал. Тем более это не тот спор, в котором открывается правда. Здесь скорее два человека с разными взглядами пытаются доказать истинность только своего мнения, что само по себе бессмысленно.

Но если по сути говорить, то если мы разделяем нашу систему по стратегическим паттернам DDD, то в ходе такого разделения у нас может появиться несколько предметных подобластей, некоторые из которых будут больше технического характера, а не бизнесового. Например, сервис обработки и хранения файлов, авторизации и прочее. То есть они не несут как таковую бизнес-направленность и, как мне кажется, они скорее пострадают от попыток проектирования их с помощью Rich Model.

Если вы под бизнес-логикой понимаете что-то другое, то у вас и будет не DDD, а какой-то свой подход.

Бизнеслогика - это автоматизированная логика бизнеса инкапсулированная в модель бизнеса. Автоматизация может содержать свою специфику такую, как логирование, транзакции, исключения и прочее и прочее - все это бизнеслогика. У вас, если отбросить словесную шелуху, есть только два подхода:

  • излагать бизнеслогику в use case-ах, тем самым нарушая инкапсуляцию доменной модели

  • соблюсти инкапсуляцию доменной модели и оставить use case-ы условно однострочными (должен быть только один вызов метода доменной модели, обработка исключений и форматирование выходных данных для возврата клиентскому коду) - это и есть Rich Model.

Можете для интереса еще почитать про DDD трилемму.

Допустим, у какой-то сущности есть статус "На модерации". В это время пользователь не может ее редактировать. При попытке изменить любые данные API должно возвращать ошибку "Сущность находится на модерации" (допустим у нас сложный агрегат, и есть разные методы редактирования). Но прохождение модерации означает изменение статуса, а это тоже редактирование, но его делать можно. При этом самому пользователю не должно быть разрешено менять этот статус. Тут нужные проверки зависят от действия.

Извините, но это какая-то софистика. Если сущность на модерации, то это не означает, что она не может быть изменена. Это означает лишь то, что она на модерации. Доменная модель может бросать исключение, если вы вызываете ее методы, которые должны изменить ее состояние от имени клиента, если статус модели "на модерации". Это обычная State Machine. Но в то же время модель может получать изменения от операторов back office-а.

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

Почитайте про нарушение инкапсуляции, чтобы понять, что это не так. Логика вынесенная из объектов в сервисы - это вынос инвариантов объекта вовне. Вследствие чего объект не может отвечать за свое состояние. Вместо него за инварианты отвечает облако сервисов вокруг него. А учитывая, что один и тот же метод объекта неизбежно будет вызываться в разных сервисах, мы получаем дублирование кода при попытке соблюсти инварианты. А потом получаем и проблему с поддержкой. Это когда логика соблюдения инвариантов размазана по всему проекту и программист будет бояться изменить что-то в одном месте, так как не будет знать во скольких местах это еще исправить, чтобы проект не посыпался. Про unit тестирование я вообще не говорю - тестирование сервисов application слоя - самая бесполезная вещь. Что полезнее тестировать: модель или кучу подпорок в виде сервисов, которые не дают ей упасть?

Бизнеслогика - это автоматизированная логика бизнеса инкапсулированная в модель бизнеса

У бизнес-логики есть общепринятое определение, я его приводил выше в ветке.

тем самым нарушая инкапсуляцию доменной модели

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

должен быть только один вызов метода доменной модели

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

Если сущность на модерации, то это не означает, что она не может быть изменена.

Я не сказал, что она не может быть изменена. Я сказал, что она не может быть изменена пользователем. Вы не поняли проблему.

Доменная модель может бросать исключение

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

которые должны изменить ее состояние от имени клиента, если статус модели "на модерации"

Это и означает, что правила валидации модели зависят от действия, о чем я и говорил.

Логика вынесенная из объектов в сервисы - это вынос инвариантов объекта вовне.

Вы зачем-то начали спорить, не прочитав дискуссию. Разговор был как раз о том, что это не инварианты объекта.

Почитайте про нарушение инкапсуляции, чтобы понять, что это не так.

Я про это читал, и именно поэтому не согласен и высказываю возражения.

А учитывая, что один и тот же метод объекта неизбежно будет вызываться в разных сервисах, мы получаем дублирование кода при попытке соблюсти инварианты.

Нет, не получаем. В одном действии бизнес-логики разрешено вызывать некоторый сеттер, в другом нет.

Это когда логика соблюдения инвариантов размазана по всему проекту

С логикой в сервисах она не размазана по всему проекту.

так как не будет знать во скольких местах это еще исправить

Исправить ему надо в одном действии, которое работает неправильно.

Что полезнее тестировать: модель или кучу подпорок в виде сервисов, которые не дают ей упасть?

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

У меня есть статья про это, там описаны 6 небольших бизнес-действий. Хотите обсудить архитектуру кода, пишите код, обсудим. Можете реализовать только одно действие "Отправка на ревью", этого будет достаточно. Реализация должна быть полностью от входных данных для API до сохранения в базу. Если хотите, можете написать и тест.

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

Почему возник этот вопрос. Рассмотрим приложение с визуальным интерфейсом. На форме располагаются несколько панелей с данными. В каждой панели отображаются данные отдельного агрегата. То есть на форме представлены данные нескольких агрегатов.

Но с панелью связан не сам агрегат, а объект view model, который наполняется данными из агрегата. И как раз раз не агрегаты, а объекты view model могут участвовать в обмене событиями, если в одной из панелей происходит изменение данных. Эта панель отправляет события другим панелям и их объекты view model будут меняться.

Не могу представить себе функционал в котором агрегаты взаимодействуют между собой при помощи событий внутри приложения. Может кто-то сталкивался с подобным функционалом?

Мне кажется, что в таком случае рассмотренные способы вряд ли будут работать как надо. Возможно, в вашем случае подойдет паттерн Publisher-Subscriber? Вы также создаете класс Publisher, но который не хранит события, а просто выполняет их пересылку по подписчикам. А ViewModels могут подписываться на события изменения.

Извиняюсь за большой размер примера, но вот так, например (пример только на C# могу сделать):

// Определения событий
public record Event1(string Name);
public record Event2(int Age);

// Интерфейс обработчика события.
// В generic типе - тип обрабатываемого события
public interface IEventListener<in TEvent>
{
    void HandleEvent(TEvent @event);
}

// Интерфейс издателя событий.
// На него могут подписаться обработчики, а он
// затем вызовет метод обработки событий
public interface IEventPublisher<TEvent>
{
    void Subscribe(IEventListener<TEvent> listener);
    void RaiseEvent(TEvent @event);
}

// Издатель первого события
public class Event1Publisher : IEventPublisher<Event1>
{
    // Коллекция подписчиков
    private readonly ICollection<IEventListener<Event1>> _listeners = [];
    
    public void Subscribe(IEventListener<Event1> listener)
    {
        _listeners.Add(listener);
    }

    public void RaiseEvent(Event1 @event)
    {
        foreach (var listener in _listeners)
        {
            listener.HandleEvent(@event);
        }
    }
}

// Издатель второго события
public class Event2Publisher : IEventPublisher<Event2>
{
    private readonly ICollection<IEventListener<Event2>> _listeners = [];
    
    public void Subscribe(IEventListener<Event2> listener)
    {
        _listeners.Add(listener);
    }

    public void RaiseEvent(Event2 @event)
    {
        foreach (var listener in _listeners)
        {
            listener.HandleEvent(@event);
        }
    }
}

// Первый ViewModel. Тут мы реализуем интерфейсы с
// generic типами тех событий, которые мы будем обрабатывать
public class ViewModel1 : IEventListener<Event1>, IEventListener<Event2>
{
    private readonly IEventPublisher<Event1> _event1Publisher;
    
    public ViewModel1(IEventPublisher<Event1> event1Publisher)
    {
        // Подписываемся на события издателя
        _event1Publisher = event1Publisher;
        _event1Publisher.Subscribe(this);
    }
    
    public void HandleEvent(Event1 @event)
    {
        Console.WriteLine($"HandleEvent {@event.Name}");
    }

    public void HandleEvent(Event2 @event)
    {
        Console.WriteLine($"HandleEvent {@event.Age}");
    }

    public void DoSomeOperation()
    {
        // В процессе выполнения какой-то операции
        // нам нужно изменить данные в остальных связанных ViewModel'ях 
        _event1Publisher.RaiseEvent(new Event1("Alexander"));
    }
}

// Второй ViewModel. Тут мы только слушаем события, но не вызываем их
public class ViewModel2 : IEventListener<Event2>
{
    public ViewModel2(IEventPublisher<Event2> event1Publisher)
    {
        event1Publisher.Subscribe(this);
    }
    
    public void HandleEvent(Event2 @event)
    {
        Console.WriteLine($"ViewModel2 HandleEvent {@event.Age}");
    }
}

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

Мой вопрос немного о другом - в какой бизнес-логике событиями будут обмениваться агрегаты, а не объекты ViewModels? Про ViewModels всё понятно. Очень часто используется обмен событиями/сообщениями для изменения данных в ViewModels.

В литературе по DDD очень часто говорят об обмене событиями именно между агрегатами.

На форме располагаются несколько панелей с данными.
объекты view model могут участвовать в обмене событиями, если в одной из панелей происходит изменение данных.

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

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

Большого смысла делать это событиями нет. Если есть контроль над кодом с бизнес-логикой, то лучше написать все вызовы явно. Чтобы программисту не надо было искать по всему приложению, если бизнес скажет "У нас там при таких-то условиях статистика не обновляется".

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

Описанное далее рассмотрю на примере десктоп приложения. После обновления данных на визуальной форме обновляются данные во view model и далее они синхронизируются с соответствующим агрегатом в logic layer. В этот момент агрегат генерирует доменное событие. Типовой вариант логики - это запись события в хранилище событий. Но в литературе по ddd любят рассказывать о том, что агрегаты обмениваются доменными событиями между собой. Могу себе представить какую-то гипотетическую ситуацию такого обмена. Но для той логики, которую я видел в реальных проектах, пока не могу найти ситуацию, когда внутри приложения один агрегат пересылает событие другому агрегату.

Например, надо сохранить использованный в заказе метод оплаты для следующих покупок. Метод оплаты может быть отдельной сущностью или частью агрегата Customer. Или в Customer есть денормализованное поле для количества заказов, и его надо обновить. Это больше связано с принципом DDD "Поля сущности надо менять только из ее методов" и "Один агрегат не может напрямую вызывать методы другого агрегата", чем с реальной необходимостью использовать события. Но код для приема события находится не в сущности, а в отдельном обработчике, который вызывает метод сущности.

У меня пока получилось гипотетически описать один подобный алгоритм. И только для варианта десктопного приложения.

Необходимо провести расчёт двух сущностей, которые в коде описаны как агрегат Agg1 и агрегат Agg2. Расчёт идёт по шагам - вначале рассчитывается Agg1 и затем Agg2.

  1. Открывается форма1 и на ней вводятся исходные данные для расчёта Agg1. Запускается алгоритм расчёта Agg1.

  2. Результаты расчёта вносятся в агрегат Agg1. Затем создаётся агрегат Agg2 заполненный данными по умолчанию, объект Agg1 генерирует доменное событие и в Agg2 запускается обработчик этого события. В результате обработки события в Agg2 вносятся данные, полученные от Agg1 через содержимое события.

  3. Открывается форма2 и на ней вводятся исходные данные для расчёта Agg2. Запускается алгоритм расчёта Agg2. В этом расчёте наряду с данными, введенными на форме, используются также и данные, полученные от Agg1 при обработке доменного события.

  4. Алгоритмы расчёта Agg1 и Agg2 реализованы в виде методов этих агрегатов.

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

Потому и говорю, что это будет работать только в десктопном приложении.

Приведенный мною пример подходит только для одного типа приложения. Об этом сразу же и предупредил.

Агрегат1 это например вентилятор, а агрегат2 - кондиционер. Но это не имеет особого значения потому что это попытка понять, как агрегаты могут взаимодействовать при помощи доменных событий.

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

Приведенный мною пример подходит только для одного типа приложения.

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

как агрегаты могут взаимодействовать при помощи доменных событий

Вопрос "как" описан в данной статье - создаем событие, отправляем в главный обработчик, он вызывает конкретный обработчик, он вызывает метод другой сущности. На вопрос "зачем" я ответил выше - обновить поле "customer.order_count" при создании заказа без использования сервиса, который работает одновременно с 2 агрегатами Order и Customer. Но повторюсь, я считаю, что правильнее сделать сервис.

Возможно, вы просто считаете избыточным создавать объект Ventilator, когда у вас уже есть объект VentilatorView и передаете его в логику, которая на самом деле должна принимать Ventilator, но это неправильно.

Выше было указано - "Результаты расчёта вносятся в агрегат Agg1." Конечно же view object не может передаваться в persistence layer для сохранения в бд. Для этого данные из view object передаются в объект с которым работает слой persistence layer.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации