Comments 46
Поделитесь, пожалуйста, примером агрегата с 5 (или больше) уровнями вложенности. Просто для расширения кругозора
один Use Case должен менять 5+ агрегатов, а не сложный агрегат
CRM пример придумал - смена владельца аккаунта:
Account - переводится другому менеджеру
Contact - все контакты в аккаунте переходят менеджеру
Deal - все сделки в аккаунте переходят менеджеру
ActivityLog - история активностей перепривязывается
TeamQuota - квоты старого и нового менеджера пересчитываются (выручка, количество аккаунтов)
Здесь действительно нужна полная консистентность. Иначе:
Менеджер видит аккаунт, но контакты не его
Квота неправильная
Отчеты врут
Хорошо сформулированы проблемы.
Меры борьбы:
EF - write sise, dapper + view (вместо спецификаций) read side
Файлы объединяем - Command + Handler + Validator,
Query + Handler + DTO, сущность отдельноодин Use Case должен менять 5+ агрегатов => доменные события или outbox или Saga, но вероятно в полной мере бизнесу это не нужно (я не встречал)
Несколько соображений.
Лучше не тащить orm, особенно в ddd. Да, ef core крут и даст фору многим, но все-таки каждый инструмент под свою задачу. Решился на ddd, тогда не надо экономить на sql.
Попробовать перестать крутиться вокруг агрегатов, как самых что ни на есть самых самостоятельных участников системы и дать шанс сущностям (вот этим несамостоятельным) - размыть границы. Это даст гибкости.
Забыть про dto. 1001-е лихо сформированное дтошка добьет проект и ddd тут не спасет. Dto в больших проектах нет. Есть request, response. И тут появляются хэндлеры - да, но не mediatr.
Пересмотреть репозитарии и их имплементацию, отстуствие orm дает крутые бонусы и отличную управляемость с производительностью, если опять же слепо не следовать канонам и не тащить агрегаты на каждый запрос. Как следствие появляется некоторый промежуточный инструмент компромисса: response. Но с ним надо быть осторожно. Здесь больше про оптимизацию.
Не забывать про ValueObjects. Бизнес скажет спасибо.
Профит.
Решился на 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 + дружественные сборки - помогут предовратить неправильную эксплуатацию аггрегата.
>Мне казалось, что 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
Особенно смешно, когда кроме одной БД на проекте ничего и нет :)
Я за свою жизнь ни разу не встречал проекта, которому бы хватило одной БД для всего. Абсолютно на всех местах работы системы вроде как и декларировались самодостаточными, но по факту зависели от других. И в итоге постоянно решались проблемы синхронизации данных между ними. Обычно костылями. Так что я давно уже не верю, что можно "транзакцией в БД" решить все проблемы. А уж в нынешнее время, когда микросервисы пихают всюду, и где это оправдано, и просто чтобы быть "как все" - можно смело забывать эти преимущества и привыкать работать по новому)
Я со всем согласен. Просто не понимаю, зачем усложнять себе жизнь при работе в одной БД на ровном месте. На интеграции с другими сервисами хватит развлечений :)
Транзакцией в БД все проблемы, конечно, не решишь, а вот если на транзакции в БД забить, то можно поиметь очень много новых проблем на пустом месте.
Зачем вообще агрегат для чтения? Вызываешь репозиторий для этой сущности и читаешь что надо
Что видится мне:
Что-то не то с уровнями вложенности. 5-6 уровней - возможно, агрегат стоит пересмотреть и разбить его на более мелкие.
Если есть необходимость менять сразу несколько агрегатов, то стандартный подход - доменные события. Не настолько это затратно по разработке, чтобы бизнес встал на дыбы. Тем более речь идёт о достаточно тяжелом приложении с аж 30+ агрегатами. Вложишься сейчас, сэкономишь время разработчика в будущем.
Свалка из сотен классов спецификаций может быть сгруппирована поагрегатно
Неоптимизированные запросы в случае спецификаций - известная проблема, которая решается упрощением агрегатов.
Если вас беспокоит невозможность переиспользовать спецификации в dapper, рассмотрите отказ от dapper в пользу любого другого orm с поддержкой iqueryable. Мне не совсем понятно, зачем при наличии EF вам второй orm, но если что то легковесное нужно - linq2db есть
"DDD подразумевает глубокую структуру папок по слоям" - вообще нет. Это clean architecture. Но вообще разделение на слои, наоборот, помогает ориентироваться в коде
"«Добавить поле в сущность» требует изменений в 10-15 файлах" - в большом коммерческом продукте это хорошо и правильно. Если у вас возникает очень часто такая необходимость, рассмотрите возможность работы с сущностями с динамическим набором атрибутов
Бизнесу нужно, чтобы при создании «Заказа» одновременно создавался «Платеж», резервировался «Товар» на складе и создавалась «Задача» в колл-центре.
Постоянно слышу от противников/сомневающихся в DDD подобные примеры, что он, дескать, не работает в реальном мире. Да, наверное, где-то и есть такой бизнес, где абсолютно все реализуется в одной системе и можно решать задачи одной транзакцией.
Только вот в реальном мире на складе одна система, платежами заведует другая, колл-центром третья, половина из них - куплены как готовые решения (и часто без штатных механизмов интеграции). Ну, и как в таких условиях будете решать эту задачу? Поможет тут DDD или нет?
Если декомпозиция сделано хоть как-нибудь правильно, то нам удобно в своём сервисе работать в транзакции, а с остальными сервисами "интегрироваться". Под интеграцию закладывается отдельное время разработки, т.к. вообще хз что может вылезти. А потом ещё и тестить тяжко и стрелять может долго.
Интеграция всегда дороже, чем "сохранить всё в одной транзакции". Поэтому нужно понимать, что мы получаем в замен, когда начинаем в стиле "интеграции" внутри одного приложения.
Для большинства моих задач для интеграции с другими сервисами мне хватает TransactionOutbox, позволяя надёжно выполнить изменения в другой системе после фиксации в моей (положить сообщение в очередь, дёрнуть внешний метод и т.п.).
Я имел в виду, возражения в духе "бизнесу нужно чтобы одновременно" - через DDD не решаются или решаются неудобно. Но по факту, оказывается что "бизнес" задержка в "одновременности" в пару секунд - вполне устраивает. И в итоге DDD вполне работает и облегчает разработку. Агрегат сохраняется вместе с событиями (тот самый outbox) в одной транзакции в свои таблички, а после этого события расползаются по связанным агрегатам и системам, производя свои изменения неявно для первого агрегата. Система стает слабо связанной и более устойчивой к сбоям.
Точно то же самое реализуется без DDD на хендлерах или юскейсах (какой-то обработчик какой-то команды). Делаем часть работы, кидаем событие или команду в outbox, где-то произойдёт дальнейшая обработка. Получаем свою "согласованность в конечном итоге" и все связанные с этим проблемы и преимущества.
Что будет внутри обработчика вообще без разницы: 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-части, там где речь идёт о важности дистилляции понятий и моделей. К тактическим паттернам немало вопросов, но в книге для меня они не главные.
DDD ASP.NET Почему не удобно для больших проектов с более чем 30 реестрами