Pull to refresh

Comments 46

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

один Use Case должен менять 5+ агрегатов, а не сложный агрегат

В тексте статьи четко написано про 5-6 уровней вложенности

И то и другое написано.

В общем, примеры интересно послушать в любом случае

CRM пример придумал - смена владельца аккаунта:

  1. Account - переводится другому менеджеру

  2. Contact - все контакты в аккаунте переходят менеджеру

  3. Deal - все сделки в аккаунте переходят менеджеру

  4. ActivityLog - история активностей перепривязывается

  5. TeamQuota - квоты старого и нового менеджера пересчитываются (выручка, количество аккаунтов)

Здесь действительно нужна полная консистентность. Иначе:

  • Менеджер видит аккаунт, но контакты не его

  • Квота неправильная

  • Отчеты врут

Тут не один агрегат, а много. И налицо событийная модель.

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

Вы хотите пример с глубокой вложенностью агрегатов? Ну это бред

Хорошо сформулированы проблемы.

Меры борьбы:

  • EF - write sise, dapper + view (вместо спецификаций) read side

  • Файлы объединяем - Command + Handler + Validator, Query + Handler + DTO, сущность отдельно

  • один Use Case должен менять 5+ агрегатов => доменные события или outbox или Saga, но вероятно в полной мере бизнесу это не нужно (я не встречал)

Несколько соображений.

  1. Лучше не тащить orm, особенно в ddd. Да, ef core крут и даст фору многим, но все-таки каждый инструмент под свою задачу. Решился на ddd, тогда не надо экономить на sql.

  2. Попробовать перестать крутиться вокруг агрегатов, как самых что ни на есть самых самостоятельных участников системы и дать шанс сущностям (вот этим несамостоятельным) - размыть границы. Это даст гибкости.

  3. Забыть про dto. 1001-е лихо сформированное дтошка добьет проект и ddd тут не спасет. Dto в больших проектах нет. Есть request, response. И тут появляются хэндлеры - да, но не mediatr.

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

  5. Не забывать про ValueObjects. Бизнес скажет спасибо.

Профит.

  1. Решился на ddd, тогда не надо экономить на sql.

А как собирать агрегаты и потом сохраняться изменения? Мне казалось, что DDD без ORM не живут.

Ну, если подразумевать агрегат по Эвансу, то рецепт его сборки из бд следующий: оптимизированная CTE, желательно Dapper (удобен он, не отнять), и дальше в репозитрии сборка агргегата через CollectionsMarshal и спанов с использование конструкторов или стат методов агрегата. Производительность по сравнению с ef core ощутимая. При реализациии сразу видишь и бд (запрос) и агрегат - уже понимаешь варианты оптимизаци.

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

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

с использование конструкторов или стат методов агрегата.

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

Как собрать - я понял, а как сохранять без ченж трекинга? Я вижу 2 варианта: сохранять всё или знать снаружи, что нам нужно сохранить. Может знаешь как проще?

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

1)

>>> а как сохранять без ченж трекинга? 

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

Каждый агрегат унаследован от RootAggregate в котором есть: IReadOnlyCollection<IDomainEvent> Events { get; } void ClearDomainEvents(); void AddDomainEvent(IEvent @event); - это в доменном слое. А дальше уже Application слой. Подписка идет через специальные EventHandler (дженерик обычно, где типы TAggregate и TDomainEvent). Обрабтываются события в апликейшн на уровне UnitOfWork в конкретном хендлере.

2)

>>> Нужно будет защититься

Однозначно - да. У нас же не го - мы себе можем позволить базовый комфорт. Пример: связка internal + дружественные сборки - помогут предовратить неправильную эксплуатацию аггрегата.

Спасибо за ответ, про такое решение не подумал

Ну и как сохранять то, без ChangeTracker? Хочу услышать ответ. Написать свой ChangeTracker вместо ORM?

>Мне казалось, что DDD без ORM не живут.

Вот это интересный момент, конечно. Один мой коллега-дотнетчик, когда его отправили учить жизни php-шников, в разговоре с их начальником проронил такую фразу: "ну, я не знаю, есть у вас ORM или нет". От этого по его мнению зависело, можно на их проекте внедрить DDD или нельзя.

И тут корень противоречия. DDD на "философском уровне" про "Tackling Complexity in the Heart of Software" и про полное абстрагирование от способов хранения. А на практике - "если у вас нет ORM, то нет DDD". И ведь действительно очень сложно реализовать стандартные DDD-паттерны проектирования без ORM, если у вас не документоориентированная база данных.

Странная тема обсуждения "DDD с/без ORM". DDD (логика домена) находится в logic layer. ORM используется в persistence layer. Логика домена, когда в неё приходят данные, полученные из бд, вообще не имеет никакого представления о том, что эти данные были получены при помощи ORM или какими-то другими механизмами.

Так очевидно же. Как выглядит типичный код с применением DDD?

var request = repository.GetRequest(requestId);
request.Approve(userId);
repository.SaveChanges();

Ну и как вы в данном случае реализуете репозиторий без ORM? ChangeTracker в рукопашку забабахаете?

Ну и сами "доменные модели". Я застал ещё время, когда вся эта доменка представляла собой набор свойств с {get; set}и конструктором по умолчанию. Потому что EF (без Core) ничего сложнее мапить не умел. Сейчас EF многому научился, но уши персистентности из моделей всё равно там и тут торчат. Вот и получается, что проектирование _моделей_, самого сердца DDD, прочно связано с возможностями ORM.

Если размывать ради гибкости SQL-запросов на чтение - да, CQRS read side может обходить агрегаты. Если размывать на записи - это путь обратно к transaction script

Value objects для making illegal states unrepresentable внутри домена - конечно!

 CQRS read side может обходить агрегаты

Получается, что наш домен - это наша БД?

Я к тому, что читать тоже нужно уметь и логика чтения и логика в агрегатах может будет дублироваться.

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

А еще вместо табличек (реляционной БД) может быть вообще что угодно. Хоть текстовые файлы определенного формата. И ничего при смене технологии хранения менять не придется, кроме реализации "репозитория". И да, в серьезных проектах - иногда приходится менять систему хранения на более подходящую. И в итоге часть агрегатов живет в postgresql, часть в elastic, а часть вообще в другой системе с доступом по rest api. А в слое домена все однотипное.

Это я понимаю.

Из-за этого мы и не пользуемся преимуществами реляционных БД. Фактически создаём себе ограничения, а потом героически их преодолеваем :D

Особенно смешно, когда кроме одной БД на проекте ничего и нет :)

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

Я со всем согласен. Просто не понимаю, зачем усложнять себе жизнь при работе в одной БД на ровном месте. На интеграции с другими сервисами хватит развлечений :)

Это скорее был пример, что не стоит делать архитектуру вида "спагетти". Даже в одной БД имеет смысл разделение на модули. Транзакции, затрагивающие 50 таблиц тоже ведь так себе решение.

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

Аналогично, пришел со временем к такой же архитектуре

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

Зачем вообще агрегат для чтения? Вызываешь репозиторий для этой сущности и читаешь что надо

Что видится мне:

  1. Что-то не то с уровнями вложенности. 5-6 уровней - возможно, агрегат стоит пересмотреть и разбить его на более мелкие.

  2. Если есть необходимость менять сразу несколько агрегатов, то стандартный подход - доменные события. Не настолько это затратно по разработке, чтобы бизнес встал на дыбы. Тем более речь идёт о достаточно тяжелом приложении с аж 30+ агрегатами. Вложишься сейчас, сэкономишь время разработчика в будущем.

  3. Свалка из сотен классов спецификаций может быть сгруппирована поагрегатно

  4. Неоптимизированные запросы в случае спецификаций - известная проблема, которая решается упрощением агрегатов.

  5. Если вас беспокоит невозможность переиспользовать спецификации в dapper, рассмотрите отказ от dapper в пользу любого другого orm с поддержкой iqueryable. Мне не совсем понятно, зачем при наличии EF вам второй orm, но если что то легковесное нужно - linq2db есть

  6. "DDD подразумевает глубокую структуру папок по слоям" - вообще нет. Это clean architecture. Но вообще разделение на слои, наоборот, помогает ориентироваться в коде

  7. "«Добавить поле в сущность» требует изменений в 10-15 файлах" - в большом коммерческом продукте это хорошо и правильно. Если у вас возникает очень часто такая необходимость, рассмотрите возможность работы с сущностями с динамическим набором атрибутов

Бизнесу нужно, чтобы при создании «Заказа» одновременно создавался «Платеж», резервировался «Товар» на складе и создавалась «Задача» в колл-центре.

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

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

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


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

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

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

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

Что будет внутри обработчика вообще без разницы: DDD агрегаты, вызов хранимки или призыв Ктулху.

Принципиально другим подходом я бы назвал использование оркестратора: комунды или темпорала. Но опять таки, у нас оркестрация вместо хереографии, а по факту дёргаются обработчики, которые внутри могут быть как угодно написаны.

Ну, в общем то да. DDD просто снижает когнитивную сложность.

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

Даже подкаст писал на эту тему.

один Use Case должен менять 5+ агрегатов.

Наверное это ключевой вопрос, который возникает при использовании DDD. Юз кейс работает с ключевым агрегатом, но при его обработке могут меняться и другие агрегаты. Следуя логике 1 агрегат = 1 транзакция в этом юз кейс в бд сохраняется только ключевой агрегат. По остальным агрегатам формируются доменные события с описаниям изменений агрегатов и эти события сохраняются таблицу событий Outbox. Здесь и возникает вопрос - кто будет просматривать эти данные в Outbox и вызывать юз кейсы для изменения каждого агрегата. Здесь логически продолжаю цепочку - у каждого юз кейс только один агрегат изменяется в транзакции. Значит для изменения каждого агрегата должен быть отдельный юз кейс. Если архитектура микросервисная у которой 1 микросервис=1 агрегат, то можно дёргать нужный микросервис для каждого изменённого агрегата. А если архитектура монолитная - то как?

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

А если архитектура монолитная - то как?

А в чём проблема? На аутбокс событие есть подписчик, который дёргает юскейс.

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

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

А если агрегаты находятся в разных бд?

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

Вот у меня есть сценарий - принятие пользователем приглашения о присоединении к системе.

Пользователь проходит по ссылке-приглашению, в результате запись из таблички invitations удаляется, а запись в табличку users добавляется. Кто тут кому root aggregate и как через DDD должны быть организованы события, чтобы всё это дело не рассыпалось? Как это сделать без DDD я и так знаю.

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

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

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

В итоге у вас 2 бумажки - приглашение и копия карточки вашей учётной записи.

Почему же ваш услужливый оператор работает сразу в двух агрегатах ("создаёт учётную запись" и "отмечает номер учётной записи в своей табличке с приглашениями")? Кто такой этот "оператор" в программном коде?

Что за агрегаты? У меня тут в апи бд с 300+ таблицами и все прекрасно работает в ddd. Просто надо не загружать сущности все сразу, ef умеет делить запросы, в ef можно ef sql писать!

Скорее всего вам надо было просто по другому определить aggregate root или выделить новые аггрегаты.

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

А молодежь - все идем читать Эванса и Вернона)

Подпишусь под многим. Правда, тут не в ASP.NET и не в EF дело. И даже не в DDD, а в IT-евангелизме как таковом.

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

Вообще, я вообще очень люблю книгу Эванса, но больше в soft-части, там где речь идёт о важности дистилляции понятий и моделей. К тактическим паттернам немало вопросов, но в книге для меня они не главные.

Sign up to leave a comment.

Articles