Search
Write a publication
Pull to refresh

Comments 77

зачем вам возвращать IQueryable из методов вашего репозитория?

Вот зачем

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

orders.Where(o => 
             o.Created.Year < fiveYearsAgo 
             && o.State = OrdrerState.Coordination
             && o.AccountManager.Fired);

Я так понял, что по DDD нужно (рекомендуется) иметь отдельный метод под каждое назначение:

interface IOrdersRepository
{
  Order[] GetOrdersOfFiredManagers(int year, OrdrerState state);
  Order[] GetOrdersSortedByEndDate(int year);
  Order[] GetOrdersByManager(string accountManagerName);
}

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

В любом случае, если рассматривать IQueryable как инструмент (коим он и является), то с ним у меня есть возможность писать в логике условия, а без него - нет.

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

По мнению автора статьи -- да, так мы идеологически отдаляемся от DDD. Потому что репозиторий при таком подходе недостаточное явно отражает доменные концепции ¯\(ツ)

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

Я вообще рассчитываю, что в тред придет адепт DDD и расскажет как с этой проблемой бороться.

Сам фильтр критериев отбора сам по себе не может быть "набором данных" и входить в DDD как поддомен или что-то ещё? Что этому мешает? Как последовательно развивать фильтрацию от "выбери по ИД" до "фасетного и таргетированного поиска"?

Эти вопросы скорее к автору статьи. Ради ответов на них я и вписался в дискуссию.

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

Так это... Aggregates and Repositories are relevant for C part of CQRS.

Кверики для UI вполне могут формировать представления, объединяющие лютые проекции 100500 агрегатов. Вам же в этом случае инварианты проверять и обеспечивать не требуется, так как нет изменения состояния.

Там вроде текучий интерфейс просто? Или типо того

У IQueryable, как это относится к DDD чето не могу понять

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

Для начала, DDD не знает такого понятия, как "бизнес-логика". Этот слой совершенно сознательно разбивается на два разных - логика приложения и логика домена.

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

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

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

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

////

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

Защищать агрегаты, конечно, а для этого вам придётся конструировать эффективные запросы к вашему хранилищу, не выставляя наружу потроха (в вашем случае AccountManager), а это по факту можно сделать только внутри репозитория, но никак не снаружи.

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

DDD не для вас и репозитории вам в принципе не нужны

Я бы не стал так категорично придираться к терминологии. Есть устоявшийся термин - "бизнес-логика". Это логика обработки данных в терминах бизнеса. По сути это логика домена. И что значит "репозитории в принципе не нужны"? Если я, допустим, не следую DDD, то мне репозитории не пригодятся?

Далее, вот этот вот набор фильтров имеет какой-то смысл с точки зрения доменной логики?

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

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

И что значит "репозитории в принципе не нужны"? Если я, допустим, не следую DDD, то мне репозитории не пригодятся?

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

Их много. Их сложно назвать и их незачем выносить в отдельный именованый тип.
в доменной логике или нет?

если вы не выделяете доменную логику, то вы в этом разговоре напрасно тратите и своё и моё время

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

я не знаю, сумел ли Эванс зарегистрировать торговую марку на термин Repository, на самом деле, но лично мне хотелось бы, конечно, запретить его использовать тем, кто DDD не практикует :)
но это так, лирика

если так, тогда вам не стоит лезть со своим пониманием того, что такое репозиторий, в разговоры про DDD

окей

 Domain Driven Design ...

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

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

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

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

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

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

Например, какие? Кто является главным потребителем Repository? Application Layer. Соответственно и абстракция должна быть удобной для данного слоя. Но если пойти еще дальше, то у репозитория может быть только один метод на получение - Get(Identity id). И метод записи уже зависит от того, как реализован UoW. То, как вы выбираете данные, отношения к DDD не имеет, хоть SQL запросы прямо из вьюхи делайте.

Вашим репозиториям не нужен IQueryable (и паттерн Specification тоже)

К счастью, для связки .net + ef core есть в меру кривое почти красивое решение – использовать Expressions 

Несмотря на то, что я считаю, что паттерн Specification именно в DDD не нужен, но может быть удобен, если есть четкий слой абстракции для доступа данных для Query стороны. Но в Вашем утверждении есть явное противоречие.

List

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

Опять же, Aggregate это граница транзакционности. Если вы получаете несколько агрегатов через List или аналоги, а потом обновляете их все вместе - вы нарушаете данные границы - теперь у транзакции причин завалиться в N раз больше, где N число задействованных агрегатов. Для такого случая нужно либо использовать Saga, либо делать Aggregate больше, либо пересматривать модель более детально.

Domain Events – мостик к нормальности.

Единственный неочевидный момент в этом паттерне - ...

Это далеко не самый неочевидный момент в этом паттерне. Самый неочевидный момент, это то, что все обработчики доменных событий выпоняются синхронно. Это ок если все ваше приложение живет InMemory. Ваш RAM выступает в качестве базы данных, тразакция это запись в RAM. Но если это веб-приложение или вроде того, этот эффект ловины приведет к тому, что накопится такое количество Aggregate для коммита, что вероятность ошибки транзакции из-за оптимистичной конкурентности (а я надеюсь она у читателя настроена) стремится к бесконечности. По схеме как будто бы не понятно, в какой момент происходит транзакция. Описанный мой флоу такой command handler -> uow -> event handler -> uow -> event handler -> uow -> коммит транзакции. Если флоу такой: command handler -> uow -> транзакция -> event handler ... , то возникает проблема гарантий доставки ивента. Т.е. у комманды A есть сайд эффект B, но если база отвалилась во время выполнения сайд эффекта B - то система зависнет в неконсистентном состоянии. Поэтому я предпочитаю интеграционные события. Доменные у меня используются только для аудит логов и заполнения Outbox.

Дальше пока устал читать и писать, может позже дополню :)

Доменное событие может и асинхронно обрабатываться, аналогично integration event, главное, чтоб без пересечения границ контекста.

Тогда без гарантий выполнения сайд эффекта. А если с гарантиями, то доменное событие превращается в интеграционное.

Кто является главным потребителем Repository? Application Layer.

Да. Равно как основным потребителем Агрегатов. Это ровно то же самое - удобный интерфейс для работы с доменным слоем, не больше, не меньше

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

Нет, никакие границы тут, разумеется, не нарушаются, да и с чего бы вдруг? Я понимаю, что это ваша трактовка, но в ней же нет никакого практического смысла (с такой ошибкой в восприятии DDD я, к слову, ещё не сталкивался).

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

Самый неочевидный момент, это то, что все обработчики доменных событий выпоkняются синхронно

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

Может ли это привести к ошибкам при параллельном сохранении разных объектов? В зависимости от вашей схемы это может быть как вполне вероятно, так и крайне маловероятно.

Если это в вашем конкретном случае вероятно, значит, вам следует разорвать транзакционный контекст на несколько с помощью интеграционных событий и паттерна outbox. Если это (а как правило так и есть) маловероятно - просто обеспечьте логику retry на исполнение команд при обнаружении исключений, связанных с этим, это легко реализуется и встраивается в command execution pipeline.

Нет, никакие границы тут, разумеется, не нарушаются, да и с чего бы вдруг?

Вот действительно. Например, мы хотим архивировать аккаунт юзера в телефонии. Берём аккаунт, берём его историю звонков и архивируем. Что может пойти не так?

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

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

  • Ваша система масштабируется, юзер хранится на одном инстансе одной базы, а история в другом; или например юзер в MySQL а история в elastic search. В вашем подходе абстракция течет, говоря о том, что строго настрого для выполнения операции архивации пользователя он сам и его история должны принадлежать к одному UoW. Т.е. не получится партиционировать историю звонков по годам. Строго говоря, в домене такого ограничения нет. Когда появится необходимость оптимизировать хранилище (инфраструктуру), записи за 2025 год хранить на теплом инстансе, а за 2023 в холодном сторадже, придется объяснить бизнесу, что для этого придется переписать кусок логики.

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

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

Да, разумеется, и это главная фича доменных событий.

Жизнь заставит вас пересмотреть свои взгляды, когда наиграетесь в DDD на локалхосте и начнёте масштабироваться. Когда окажется, что одни данные в MySql, другие в elastic, mongo и тд, синхронные макеты вам не помогут. А если начнёте с одной базы и синхронных макетов, готовьтесь переписать под приложения, чтобы внедрить поддержку асинхронности на должном уровне.

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

В этом месте корабль DDD затонул из-за дырявой абстракции. В этом месте вы явно объявляете, что раз модуль А ловит событие модуля Б синхронно, то одно из двух: либо есть риски неконсистетного состояния, либо ваш UoW обслуживает оба модуля. Во втором случае это текущая абстракция. Модуль А независимым от модуля Б, но по факту неявно они делят контекст и без одного не может быть другого. И когда у вас произойдет смена хранилища из-за разных требований к масштабированию модулей (деманд модуля Б растет, а модуля А нет) - придется лопатить весь апликейшен код.

Текущая абстракция = при изменении инфраструктуры, неизбежно изменение application code.

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

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

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

Извините, конечно, но вы упорно путаете тёплое с мягким.

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

Агрегаты защищают инварианты, а не спасают от конкурентных обновлений :)

Жизнь заставит вас пересмотреть свои взгляды, когда наиграетесь в DDD на локалхосте и начнёте масштабироваться.

Ну да, я же, убогий, за 20+ лет практики реальных-то приложений не видел...

В этом месте корабль DDD затонул из-за дырявой абстракции

А причём тут вообще DDD? А он тут совершенно и непричём. Вообще никакого отношения к вопросу не имеет.

Хотите сохранять данные в одной транзакции, чтобы не возиться с eventual consistency? DDD вам это позволяет сделать.
Сталкиваетесь с редкими ошибками конкурентного обновления? DDD вам не мешает использовать retry.
Не можете себе позволить сохранять несколько сущностей в одной транзакции (из-за конкурентных обновлений или хранения данных в разных storage)? DDD вас полностью поддерживает.

Вообще никакой проблемы нет.

Ошибки конкурентного обновления точно также могут встречаться и внутри одного агрегата - да сплошь и рядом. 

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

Агрегаты защищают инварианты, а не спасают от конкурентных обновлений :)

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

Ну да, я же, убогий, за 20+ лет практики реальных-то приложений не видел...

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

А причём тут вообще DDD? А он тут совершенно и непричём. Вообще никакого отношения к вопросу не имеет.

Имеет ещё как. DDD это не только про доменный слой. Сам по себе без оркестрации апликейшен слоем собрать какой-то мало-мальски полезные процесс не получится. Это и есть User Story. И когда вам надо менять стори, потому что поменялась инфраструктура - это текущая абстракция от инфраструктуры.

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

Стремиться надо к такому проектированию, когда инфраструктура на домен и апликейшен влияет минимально. Тогда будет расти гибкость.

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

одна из - да, но не основная, конечно

Агрегаты защищают свои инварианты, инварианты других агрегатов не должны влиять на успешность транзакции.

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

дело доменного слоя в этом вопросе - доменное событие сгенерировать, но на этом - всё

Сам по себе без оркестрации апликейшен слоем собрать какой-то мало-мальски полезные процесс не получится.

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

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

эммм... нет
ну то есть такое тоже бывает, но, в общем случае, разумеется, нет

как вы верно сказали в начале - одной из целей разработки агрегата является желание избежать конфликтов обновления
это, вроде, очевидно

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

если вероятность конкурентных обновлений низка - обновляйтесь в одной транзакции и будет вам и простота и эффективность и счастье
если вероятность конкурентных обновлений существенна - разрывайте транзакцию

ошиблись? исправляйте ошибку в application layer, доменный слой при этом останется незатронут

если вероятность конкурентных обновлений существенна - разрывайте транзакцию

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

ошиблись? исправляйте ошибку в application layer, доменный слой при этом останется незатронут

Я вам уже объяснял, что дешёвым это изменение может не быть. Потребуется изменить и UI/UX, и протащить это по системе. Так что правило надо инвертировать: сразу смотрим что за ценность у модуля, какие риски, и если их мало - делаем в одной транзакции, но как правило в demanding модулях делаем асинхронный флоу.

Ладно, я устал спорить, в данном случае это путь в никуда.

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

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

более того выше вы отстаивали, что данное решение нерабочее.

Ну, это уже совсем ни в какие ворота... как бы я мог отстаивать такую глупость, если сам так постоянно делаю? В статье чётко проведена грань между доменными событиями и интеграционными (последние могут быть in-process, тут нет никакой проблемы).

Я вам уже объяснял, что дешёвым это изменение может не быть. Потребуется изменить и UI/UX

вы объяснили неправильно

Да, если кто-то облажался настолько эпично, как вы описали, то да, конечно придётся.

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

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

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

- Опять же, Aggregate это граница транзакционности.
- Нет, никакие границы тут, разумеется, не нарушаются, да и с чего бы вдруг?

https://martinfowler.com/bliki/DDD_Aggregate.html
Transactions should not cross aggregate boundaries.

Однако как возрос накал в обсуждении. За цитату из Фаулера уже ставят минус!!!

Конкретно этот минус поставил я. Разумеется, не за цитату, а за попытку выдать своё, кхм, странное понимание этой цитаты за аргумент в споре.

"Transactions should not cross aggregate boundaries." - транзакции не должны ПЕРЕСЕКАТЬ границы агрегатов, т.е. не должны их нарушать, не должны разрушать защиту инвариантов, которую создают агрегаты. Агрегат должен меняться по принципу всё-или-ничего.

Фаулер не говорил, что транзакции не могут ОБЪЕДИНЯТЬ сохранения разных агрегатов, это было бы полной глупостью, не правда ли? Какой в этом, даже чисто теоретически, был бы смысл?

Ваше странное понимание против нашего странного понимания

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

да, но моё странное понимание - работает на практике, в реальных проектах

ваше странное понимание, по вашему же признанию, не работает и работать не должно

и теперь, внимание, следите за руками, вы настаиваете на том, что правильное понимание - это ваше понимание, то самое, в котором ddd - это неработающая, неприменимая на практике чушь

соответственно я, по вашему настойчивому убеждению, должен отказаться от работающей методики в пользу принципиально неработоспособной, просто потому, что иначе я неправильно понимаю ddd?

э-э-э-э-э... ну, спасибо, нет

Что-то я не очень понимаю, в чем мое решение нерабочее?

Мое решение как раз рабочее безотказно, но к сожалению с бОльшим оверхэдом на проектирование. Т.е. придется сразу перевести аккаунт в состояние "Архивируется". Это предотвратит дальнейшее его изменение в процессах не связанных с архивацией. Далее на интеграционное событие я отреагирую установкой триггера через пару секунд. Это позволит завершится всем операциям записи голосовой почты. Далее я точно буду знать, что новых записей голосовой почты в этот аккаунт поступать не будет, т.к. юз кейс на добавление записей проверяет статус активации аккаунта. Все, далее по вышеописанному триггеру можно безопасно пагинировать записи, зная, что никаких конфликтов не будет. Когда процесс архивации каждой записи отдельно закончится - я отправлю команду в аккаунт на изменение статуса с "Архивируется" на "В архиве". Решение масштабируемое, независимое от технологий SQL/no-SQL, с четкими границами консистентности, без подводных камней.

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

попытку выдать своё, кхм, странное понимание этой цитаты за аргумент в споре.

Приведите пожалуйста цитату из сторонних источников с вашим пониманием. Если не приведете, значит странное понимание у вас, а не у других.

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

https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation
"Many DDD authors like Eric Evans and Vaughn Vernon advocate the rule that one transaction = one aggregate and therefore argue for eventual consistency across aggregates."

https://habr.com/en/articles/800385/
"В DDD практически всё прекрасно, за исключением одного тактического паттерна: определения границы транзакций по агрегату."
И обсуждение в комментариях, что с этим делать.

Фаулер не говорил, что транзакции не могут ОБЪЕДИНЯТЬ сохранения разных агрегатов, это было бы полной глупостью, не правда ли?

Именно говорил, именно не могут, и это именно глупость, хотя в DDD считается, что нет. Только в данном случае он просто ссылается на то, что говорили другие.

However, there is nothing preventing you from committing modifications to
two or more Aggregates in a single atomic database transaction. You might choose to
use this approach in cases that you know will succeed but use eventual consistency for
all others.

Domain-Driven Design Distilled
Vaughn Vernon

Nothing except of scalability and consistency of course.

Я же вас не предлагаю не использовать это, я говорю использовать это осознанно и с большими оговорками, а не "ваше понимание странное".

Ниже я даже писал, что сам так иногда делаю, в супер простых кейсах. Ровно как и @michael_v89.

"Use eventual consistency for all other [aggregates]" как раз и означает "Aggregate в DDD это граница транзакционности".

Ну и там дальше это написано прямым текстом:
"Just understand that this is not the primary way that Aggregates are meant to be used"

Ну, т.е., иными словами.

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

Microsoft, в свою очередь, в той самой статье, которую вы привели, упоминает о том, что разные проектировщики делают это по разному, но сами они предпочитают именно мой вариант (воплощённый в их основном архитектурном примере, к слову).

Так делаю я, так делают многие другие.

Но, разумеется, признать свою неправоту - выше ваших сил. И вы продолжаете настаивать на том, что "However, there is nothing preventing you from committing modifications to two or more Aggregates in a single atomic database transaction. " означает прямой запрет это делать в дурацком ddd.

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

Иными словами, "Aggregates in DDD are meant to be used" таким образом, что "database transactions should not cross aggregate boundaries". О чем изначально и шла речь, а вы это отрицали. Говорили, что это "ваша трактовка", "ошибка в восприятии DDD" и что это "ничему не противоречит". Оказалось, что это противоречит высказываниям самих авторов книг по DDD.

Но, разумеется, признать свою неправоту - выше ваших сил.

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

рекомендует это делать в некоторых случаях.

Он говорит не "I recommend", а "You might choose, just understand that this is not the primary way". Это не выглядит как рекомендация. Как рекомендация выглядит обозначение "primary way".

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

Если вы прочитаете внимательно ветку обсуждения, то увидите, что я приводил цитату "Transactions should not cross aggregate boundaries" как возражение на ваше утверждение "никакие границы тут не нарушаются, да и с чего бы вдруг?". Вот я вам и объяснил, с чего вдруг. Выражение "should not" на русский язык переводится как "не следует", прямой запрет придумали вы сами.

Ну да, ddd в трактовке таких, как вы, действительно нерабочая хрень.

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

Ладно бы вы могли предложить работающую альтернативу, так ведь нет, вы настаиваете на том, что ddd - нерабочая хрень

1. В этой ветке я говорю только про ваше утверждение о границах транзакций.
2. В других ветках я приводил возражения не на DDD в целом, а конкретно на ваши утверждения из статьи.
3. Работающую альтернативу я предложил в своей статье. Там есть пример бизнес-требований на 6 небольших действий и работающий код с их реализацией. Если хотите сравнить, какая альтернатива более работающая, пишите свою версию в том стиле, который вы считаете DDD, сравним. Так как есть готовый код, это должно занять у вас пару часов. Обсудим, в каких ситуациях те или иные решения дадут преимущество, а в каких не дадут.
Но я подозреваю, что вы сольетесь, как и другие сторонники DDD, как только доходит до дела.

Entity – царь паттернов

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

 Диаграмма хорошо спроектированных классов всегда вызывает эстетическое наслаждение, она элегантна и лаконична.

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

 Если она относительна проста - эти дополнительные усилия никогда не окупятся. И в этом случае вам не просто не нужны хорошо проработанные entities – вам вообще не нужно отделять доменный слой от application layer, это всё – лишняя работа, целиком. Но если предметная область окажется достаточно сложна, то попытка обойтись более простыми методами может привести к тому, что ваш проект со временем флопнется...

Может быть, стоит всегда отделять один слой от другого?

Давайте посмотрим на этот вопрос, собственно, с точки зрения разработки программного обеспечения. У нас есть: (1) сущности предметной области; (2) сущности языка программирования (всякие там структуры); и (3) те сущности, которые мы выстраиваем на базе сущностей (2), чтобы воспроизвести сущности (1).

Предположим, у нес есть таблица покупок Покупки, каждая строка которой описывает отдельный купленный товар (наименование, цена, количество и стоимость). Покупки, сделанные за одно посещение (одного и того же магазина), естественным образом группируются в блоки, и мы можем ввести новую сущность ПосещениеМагазина (дата и время, название магазина). В реляционной базе данных естественным образом возникают две таблицы: основная (содержащая первичные ключи) и подчинённая (ссылающаяся на основную при помощи вторичных ключей). Мы понимаем, что стоимость — это вычисляемый столбец, который можно не хранить в базе данных, но мы можем воспринимать таблицу покупок как документальное выражение результатов действий покупателей. Далее, цены бывают разные. Мы фиксируем в таблице ту цену, по которой нам фактически продали купленный нами товар. Но есть разные цены. Есть базовая цена товара (и, при этом, на определённую дату!), а есть цена со скидкой, разные скидки (сезонные, по акции, накопительные и т.д. и т.п.). Мы можем усложнить таблицу покупок, и вести параллельный учёт различных цен, определяя фактический размер скидки простыми запросами к данной таблице. Если посмотреть в сторону магазинов, то у магазинов есть адреса (ещё одна сущность!), а это значит, что нам нужен справочник адресов. Не очень буйная фантазия представляет этот справочник как иерархический, так что при выборе адреса можно выбирать требуемый уровень (город/улица/дом/корпус/строение). Попутно заметим, что в каждом магазине может быть принята своя система наименования товаров, нам же нужны исходные данные, а это значит, что нам нужен такой же иерархический справочник Номенклатура. И ещё. Посещение магазина — это некое точечное событие. Мы может иметь единую таблицу, где описываются такие события, и тогда мы таким же единым образом будем описывать и Ваше посещение магазина, и приезд к Вам курьера, и помещение на склад продукции, и приход в научно-исследовательский институт государственного задания, по которому этот самый НИИ получает бюджетное финансирование. И это всё суть какие-то события, а это снова отдельная сущность. И если Вы её выделяете, и тратите дополнительные усилия на реализацию этой сущности (элемент семантики), то Вы получаете мощный механизм управления своими данными. Таким образом, важнейший вопрос — это вопрос о том, а какие на самом деле должны быть сущности, чтобы отобразить домены (сущности) предметной области, на сущности (классы/объекты) языка программирования.

Entity — это нечто, обладающее идентичностью и позволяющее различать объекты (не в ООП смысле), все остальные атрибуты которых одинаковы. Какой доменный концепт обладает идентичностью, зависит уже от бизнеса.

Эванс делает акцент на жизненном цикле и существовании во времени.

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

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

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

Все по факту сказано, согласен.

Предполагаю, что автор. Спасибо.

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

public class Client : Entity
{
    ...
}

У меня такой вопрос: а почему мы обнаруживаем код, явным образом описывающий логику работы приложения? И что это за код? Это код конечного реализуемого приложения? Почему в коде явным образом указывается условие поиска, почему это условие не фигурирует в пользовательском интерфейсе (как один из вариантов)?

Этот код описывает доменную логику определения того, является ли клиент VIP или нет.
Но при этом он оформлен в таком техническом виде, что портит открытый интерфейс entity, что плохо.

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

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

Я правильно понимаю, что в 1С ссылочные объекты это Entity, менеджеры объектов - Repository, а движения документов - Value Object?

я совершенно не разбираюсь в 1С и ничего не могу сказать по этому поводу.
интуитивно - очень в этом сомневаюсь, но моя компетенция в этом вопросе строго нулевая

Birthday ...

Хороший пример.

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

Ещё один такой пример — это "Фамилия Имя Отчество". В различных ситуациях, требуются различные представления (как полные, так и сокращённые).

За DbContext отдельное спасибо.

Кстати, я все-таки делаю базовый класс, который эксёпшены EFCore'а переделывает в доменные ошибки, помимо FindById еще выставляю ListByIds и ListAll потому что на практике иногда удобно забить на масштабируемость и фигачить батчи с агрегатами в одной транзакции.

И да, я бы на первое место всё-таки ставил агрегат, а репозиторий — это его скромный слуга.
И главный акцент на поведении. Моделировать поведение, состояние будет производной. Беда EF в том, что люди сразу мыслят ДТОхами, замапленными на таблицы.

И да, я бы на первое место всё-таки ставил агрегат, а репозиторий — это его скромный слуга.

это чисто методологический вопрос - как лучше объяснять

мне кажется, что объяснить смысл агрегата без понимания (или, ещё хуже, с неправильном пониманием) репозитория - намного сложнее, но это просто моё мнение

А что там с db context?

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

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

Если это ок - ну ок. Если нет - явные / неявные локи, очереди и тд.

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

При этом они настойчиво мокают IQueryable и DbSet в тестах. Уж лучше тогда интеграционные тесты с TestContainers гонять, чем тестировать моки:-)

Побольше бы примеров кода и структуры солюшена.

Например что такое QueryHandlers в Application слое и чем они отличаются от DbQueryHandlers в инфраструктуре?

А что за Application Services и чем отличаются от domain сервисов? Что скажите насчет UseCase и UserStory?

public static readonly Expression<Func<Client, bool>> IsVipExpression = 
    c => c.Status == ClientStatus.Platinum ||
         c.Status == ClientStatus.Diamond;
    
private static readonly Func<Client, bool> isVipCompiled = 
    IsVipExpression.Compile();
    
public bool IsVip() => isVipCompiled(this);

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

Как вы создаете сущность из DTO, которое пришло из контроллера? Вот тут мой коммент где я уже собрал несколько вариантов https://habr.com/ru/articles/931866/comments/#comment_28640604

Например что такое QueryHandlers в Application слое и чем они отличаются от DbQueryHandlers в инфраструктуре?

QueryHandlers ~= CommandHandler
DbQueryHandlers - метод исполнения эффективных запросов на чтение
QueryHandlers скорее всего будут использовать DbQueryHandlers, но это не точно

А что за Application Services и чем отличаются от domain сервисов?

классы-сервисы могут быть и в application-слое, почему нет?
логики в этом слое вообще немало, просто это не та логика, которую вы бы захотели обсуждать с дедком 82 лет от роду, который называет компьютеры ЭВМ

Что скажите насчет UseCase и UserStory?

Скажу, что это прекрасные термины, не имеющие особого отношения к тактическим паттернам DDD

А чем это отличается от спецификации?

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

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

Как вы создаете сущность из DTO, которое пришло из контроллера?

в application layer, в обработчике события, вызываю фабричный метод и сохраняю транзакцию

опять же, см. раздел про Read Model, некоторые методы применения Value Object, на мой взгляд, вполне допустимы

В разделе про Read Model довольно поверхностно - можно чуть более подробно?

в application layer, в обработчике события, вызываю фабричный метод и сохраняю транзакцию

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

Просто при любой работе с Позициями держать в голове необходимость пересчитать Скидку. Просто быть идеальным 24/7, что может быть проще?
Или вы можете применить паттерн агрегат по его прямому назначению и оставить возможность менять коллекцию Позиций только через его интерфейс

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

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

Хз почему мюнусят. Я сейчас конкретно в веб приложениях, когда работаю с сущностями, у которых достаточно простое поведение, использую сущности как хранилище состояний без инвариантов. А сами инварианты, стейт транзишены и регистрацию событий в outbox пишу прямо в command handler. Да, теряется гибкость использования тактических паттернов. Но модули простые, и тащить сложности их реализации в этот код не хочется. Поэтому у меня микс - самое сложное ядро с насыщенным доменом, различными реализациями стратегий, где сложность богатой доменной модели оправдана - использую ее. В случаях, когда это нафиг не надо - использую DbContext напрямую вместе с анемичной моделью. Самое страшное, что может произойти с таким модулем, это смена персистенса на такой, который не удастся подружить с DbContext на уровне конфигурации. И то, я такой модуль за пол дня перепишу на другой движок (Mongo, DynamoDB и тд). Особенно если он покрыт тестами.

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

Репозиторий это не метод сокрытия механизмов работы с базой данных

Именно метод сокрытия таких механизмов. Не всех, а некоторых.

https://martinfowler.com/eaaCatalog/repository.html

In such systems it can be worthwhile to build another layer of abstraction over the mapping layer where query construction code is concentrated.

A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection.
Client objects construct query specifications declaratively and submit them to Repository for satisfaction.

Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer.

Причём тут фильтры, сортировки и страницы, какое отношение они имеют к доменному слою и репозиториям

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

ничто не может вам помешать создать другой интерфейс, причём в application слое и в его терминах, обозвать его «I-Что-то-там-QueryHandler», и уже в его реализации обращаться к базе данных любым способом, который вы сочтёте оптимальным для вашей конкретной задачи

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

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

Нет, это означает, что есть десятки комбинаций нескольких доменных концепций. И потенциально таких комбинаций очень много.

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

Доменные концепции это часть бизнес-логики, в репозитории не должно быть бизнес-логики.
Репозиторий, как абстракция коллекции, для выборки данных должен иметь методы findById и findByQuery. Это аналог методов прямого доступа по индексу и поиска элементов по условию для обычной коллекции.
Аргументом для findByQuery можно передавать специальный объект спецификации или просто настроенный QueryBuiler из ORM.

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

Это удобно ложится на логику в сервисах. В сервисе есть метод list(), он принимает DTO с фильтром из интерфейса и настройками сортировки и пагинации, настраивает по ним QueryBuilder, передает в репозиторий, получает список сущностей. Для пагинации нужно общее количество, можно в репозитории сделать метод findByQueryWithTotal или отдельно findTotalForQuery. Так все элементы имеют свою ответственность - репозиторий представляет коллекцию, сервис содержит логику фильтров, сущность ничего не знает про списки сущностей, и бизнес-понятия не разбросаны по всему коду.

Для методов вида findSomethingBySomethingAndSomethingAndSomething() обычно получается так, что их становится много, а используются они только в одном месте кода.

Аргументом для findByQuery можно передавать специальный объект спецификации или просто настроенный QueryBuiler из ORM.

Ну и пусть будет куча методов findSomethingBySomethingAndSomethingAndSomething. Вполне может быть, что в этих find* могут появиться внутренние специфические фильтры.

Я писал код и с использование findByQuery и с использование findSomethingBy*. Второй вариант для нас был прозрачнее. Методов не так, чтобы прямо невозможно читать репозиторий. findByQuery используем, но чисто для GetList, для интерфейса, где есть список сущностей и фильтрация по полям, которых может быть спокойно и 15 и 20 (фильтрация по параметрам автомобиля например). Во всех остальных случаях findSomethingBy* из 3-4 элементов.

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

Я писал код и с использование findByQuery
Добавлять это всё в Query идея так себе.

Вы неправильно представляете, как это работает findByQuery принимает не DTO, а QueryBuilder из ORM или более высокоуровневую спецификацию, которая работает аналогично. Поэтому добавлять туда ничего не надо, и никакой проблемы с датами нет. Сервис принимает DTO с полями createdAtFrom, createdAtTo, настраивает QueryBuilder, передает в репозиторий. Делать универсальный класс со всеми возможными полями для фильтров точно не нужно, как раз из-за тех проблем, которые вы описали.

class OrderService {
  function list(OrderListFilter $filter, Pagination $pagination): OrderListDto {
    $qb = $this->entityManager->getQueryBuilder(Order::class);
    ...
    if ($filter->createdAtFrom)
      $qb->andWhere('>=', 'createdAt', $filter->createdAtFrom);
    if ($filter->createdAtTo)
      $qb->andWhere('<', 'createdAt', $filter->createdAtTo);

    $this->applyPagination($qb, $pagination);

    $orderListDto = $this->orderRepository->findByQueryWithTotal($qb);

    return $orderListDto;
  }
}

Я прекрасно представляю как работает findByQuery. В конечном итоге у вас разрастется OrderListFilter и где-то в любом случае будут возникать баги из-за разного использования фильтров. Либо у вас разрастется OrderService на кучу методов. Тогда какой смысл в OrderService? Прослойка перед репозиторием с двумя методами? Тогда какой смысл в репозитории?

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

Добавлять это всё в Query идея так себе.
В конечном итоге у вас разрастется OrderListFilter

Раз вы говорите эти фразы, значит не представляете.
Список полей в OrderListFilter зависит только от количества полей в форме в UI. Новые поля там появляются только если попросил бизнес, а не потому что программист так захотел, поэтому никуда он не разрастется. OrderListFilter это DTO на бэкенде, которое является моделью формы ввода на фронтенде, он приходит из контроллера и в идеале создается и заполняется автоматически фреймворком. OrderListFilter не создается программистом в коде других методов, и метод list() не используется программистом в другой логике для получения списка заказов. Если нужно получить список заказов, программист настраивает query builder.

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

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

OrderListFilter только для случаев где реально требуется пагинация и фильтрация.

Да, я именно так и написал "принимает DTO с фильтром из интерфейса". Метод list() из этого примера возвращает данные для страницы списка заказов например в админке, с фильтром и пагинацией. Для списка заказов в личном кабинете пользователя будет другой сервис со своим DTO, где будут другие поля, и метод list() будет также принимать текущего пользователя, чтобы добавить его id в query builder.

Все остальные запросы в различных сервисах и джобах это отдельные методы в репозитории.

Вот я и объясняю, что у них есть недостатки. Обычно в каждом таком месте нужны свои условия, поэтому удобнее использовать спецификацию.

Сначала вы жалуетесь на методы findSomethingBySomethingAndSomethingAndSomething, а потом мне втираете про list. Передавать в list DTO с фильтрами из интерфейса как вы описали это нормально. Но формировать в других местах эту DTO и вызывать этот же list - это дичь. И у меня сложилось впечатление, что вместо findSomethingBySomethingAndSomethingAndSomething вы формируете как раз DTO

Но формировать в других местах эту DTO и вызывать этот же list - это дичь.

В третий раз объясняю - OrderListFilter не создается программистом в коде других методов, и метод list() не используется программистом в другой логике для получения списка заказов.
Единственное место, где он создается - API endpoint для получения списка заказов для соответствующей страницы на фронтенде. Этот DTO принимает поля, которые отправляет фронтенд. Если есть другая страница со списком заказов с другим набором полей в фильтре, для нее будет другой endpoint и другой DTO.

И у меня сложилось впечатление, что вместо findSomethingBySomething вы формируете как раз DTO

Вместо findSomethingBySomething я формирую объект QueryBuiler.

Вместо findSomethingBySomething я формирую объект QueryBuiler.

Если формированием объекта занимается сервис, то это неправильно.

В данном случае он играет роль спецификации, поэтому всё нормально. Это соответствует тому, что пишет Фаулер: "Client objects construct query specifications".

QueryBuiler сам по себе достаточно высокоуровневая абстракция, по нему можно построить и HTTP-запрос к стороннему API, а не только SQL. Вы можете сделать отдельный класс спецификации, но он будет работать так же, как QueryBuiler, с методами andWhere и т.д.

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

QueryBuilder это более высокоуровневая абстракция, чем конкатенация строк с SQL. Вы же в курсе, что уровней абстракции много, а не только 2 "высокий" и "низкий"?

Класс спецификации это по определению уровень домена. QueryBuilder от нее ничем не отличается, кроме того, что он в другом неймспейсе, и там вместо условия по бизнес-свойству andWhere('=', 'isVip', true) будут более детальные условия по полям таблицы andWhere('in', 'status', [Status.Platinum, Status.Diamond]). Делать ли отдельный класс ради чистоты слоев дело ваше, но обычно QueryBuilder достаточно.

Слушайте, ну конкатенация строк тоже высокоуровневая абстракция. Не байты же мы объединяем. QueryBuilder достаточно низкий уровень. И если у вас сервис занимается составлением QueryBuilder и потом отправляет его в репозиторий, то это означает что слой с сервисом лишний. Пример isVip и 2 статуса не совсем корректный. И isVip и статусы это бизнес-свойства. Status.Platinum вполне себе бизнес-часть. Вот если если bool isVip, а в таблице tiny int is_vip, то пример был бы удачнее.

Но в любом случае формирование QueryBuilder в отдельном сервисе лично мне не нравится. Но вы делайте как вам удобно. Главное, чтобы бизнесу польза была.

Слушайте, ну конкатенация строк тоже высокоуровневая абстракция.

Естественно, поэтому ваше ерничание неуместно.

И isVip и статусы это бизнес-свойства.

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

формирование QueryBuilder в отдельном сервисе

Он не отдельный сервис, я же написал, что говорю про случай, когда логика находится в сервисах. То есть они в любом случае есть, они не сделаны специально для работы с репозиторием, поэтому они не "отдельные". Они содержат всю логику бизнес-действий, неважно, репозитории там используются или что-то еще. Вместо репозитория может быть SaaS сервис с GraphQL. Конвертирование OrderListFilter в DTO для GraphQL-запроса это аналог конвертирования OrderListFilter в QueryBuilder для репозитория. Этот код где-то должен быть, и удобно помещать его в сервис, не в контроллере же его писать. А сущностей с SaaS-сервисом у нас нет.

то это означает что слой с сервисом лишний

Не означает. В админке есть действия "показать список товаров, создать товар, изменить товар, удалить товар", сервис ProductService содержит логику всех этих действий. С OrderService аналогично, только там другие действия.

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

У меня в открытых вопросах остается до сих пор остается выбор способа обработки связанных агрегатов в пределах транзакции.

  1. Писать обработку всех агрегатов в слое application подряд, одного за другим

  2. Добавлять обработчики доменного события для агрегатов.

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

спасибо

ну а какие критерии могут быть, вы всё правильно говорите

если это связка A->B, то да, прямо в обработчике команды
если это что-то более сложное, то зависит от того, куда проектировщик склоняется в дихотомии оркестрация <-> хореография, т.е. либо доменный сервис, либо доменные события

оркестрация через сервис выглядит проще, но только до определённого уровня сложности/линейности этой логики

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

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

Я делаю систему и прочитав статью обнаружил что она соответствует принципам ДДД. Забавно. Выходит я неплохой разраб. Поэтому предположу что можно еще привнести в ддд (тк сам это использую):

  • Маршрутизатор/роутер для событий - удобно расширять функционал и не надо знать вообще куда посылать событие и кто его обработает

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

  • Хорошо написанные команды составляют «язык системы» и его можно записать в файл как программу или отправить на исполнение в копию системы

  • Адресация данных: желательно адресовать их не только через ИД но и через путь

  • И тд

Удачи!

Sign up to leave a comment.

Articles