COPY имеет смысл использовать как разовую загрузку очень большого количества данных, которые не нужно проверять. Если нужно проверять, то с COPY придется делать это триггерами, то есть логика будет в базе со всеми ее недостатками. Если COPY регулярно используется в приложении, значит объемы данных не очень большие, проще загружать обычным способом через приложение в транзакции.
Проверял скорость для большой таблицы, 60 колонок, 20+ триггеров. С COPY 10000 записей грузятся 10 секунд, с логикой в приложении без триггеров 4.8 секунды, сохранение в одной транзакции массовыми INSERT по 100 строк. Проверки, аналогичные триггерам, для 10000 записей в приложении занимают 100мс.
Иными словами, "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, как только доходит до дела.
https://habr.com/en/articles/800385/ "В DDD практически всё прекрасно, за исключением одного тактического паттерна: определения границы транзакций по агрегату." И обсуждение в комментариях, что с этим делать.
Фаулер не говорил, что транзакции не могут ОБЪЕДИНЯТЬ сохранения разных агрегатов, это было бы полной глупостью, не правда ли?
Именно говорил, именно не могут, и это именно глупость, хотя в DDD считается, что нет. Только в данном случае он просто ссылается на то, что говорили другие.
Тут нет высказывания о реальных явлениях. Если считать высказыванием "Во Вселенной есть такой чертеж", то да, вероятность его в 100% можно получить только наблюдением такого чертежа (то есть экспериментом). Но в данном случае его вероятность 0%, так как выражение "круглого квадрата" создает логическое противоречие, и это высказывание не имеет смысла. Оно нефальсифицируемое, его нельзя подтвердить или опровергнуть, потому что непонятно, что имеется в виду. Ну или мы можем сказать, что тут ничего не имеется в виду.
Кроме того, это высказывание неявно подразумевает, что квадраты могут быть круглыми, поэтому контр-примером можно считать факт, что квадраты не называются круглыми.
Слушайте, ну конкатенация строк тоже высокоуровневая абстракция.
Естественно, поэтому ваше ерничание неуместно.
И isVip и статусы это бизнес-свойства.
В таком случае класс, который содержит фильтры по ним, является частью домена, и нарушения слоев вообще нет.
формирование QueryBuilder в отдельном сервисе
Он не отдельный сервис, я же написал, что говорю про случай, когда логика находится в сервисах. То есть они в любом случае есть, они не сделаны специально для работы с репозиторием, поэтому они не "отдельные". Они содержат всю логику бизнес-действий, неважно, репозитории там используются или что-то еще. Вместо репозитория может быть SaaS сервис с GraphQL. Конвертирование OrderListFilter в DTO для GraphQL-запроса это аналог конвертирования OrderListFilter в QueryBuilder для репозитория. Этот код где-то должен быть, и удобно помещать его в сервис, не в контроллере же его писать. А сущностей с SaaS-сервисом у нас нет.
то это означает что слой с сервисом лишний
Не означает. В админке есть действия "показать список товаров, создать товар, изменить товар, удалить товар", сервис ProductService содержит логику всех этих действий. С OrderService аналогично, только там другие действия.
QueryBuilder это более высокоуровневая абстракция, чем конкатенация строк с SQL. Вы же в курсе, что уровней абстракции много, а не только 2 "высокий" и "низкий"?
Класс спецификации это по определению уровень домена. QueryBuilder от нее ничем не отличается, кроме того, что он в другом неймспейсе, и там вместо условия по бизнес-свойству andWhere('=', 'isVip', true) будут более детальные условия по полям таблицы andWhere('in', 'status', [Status.Platinum, Status.Diamond]). Делать ли отдельный класс ради чистоты слоев дело ваше, но обычно QueryBuilder достаточно.
В данном случае он играет роль спецификации, поэтому всё нормально. Это соответствует тому, что пишет Фаулер: "Client objects construct query specifications".
QueryBuiler сам по себе достаточно высокоуровневая абстракция, по нему можно построить и HTTP-запрос к стороннему API, а не только SQL. Вы можете сделать отдельный класс спецификации, но он будет работать так же, как QueryBuiler, с методами andWhere и т.д.
Но формировать в других местах эту DTO и вызывать этот же list - это дичь.
В третий раз объясняю - OrderListFilter не создается программистом в коде других методов, и метод list() не используется программистом в другой логике для получения списка заказов. Единственное место, где он создается - API endpoint для получения списка заказов для соответствующей страницы на фронтенде. Этот DTO принимает поля, которые отправляет фронтенд. Если есть другая страница со списком заказов с другим набором полей в фильтре, для нее будет другой endpoint и другой DTO.
И у меня сложилось впечатление, что вместо findSomethingBySomething вы формируете как раз DTO
Вместо findSomethingBySomething я формирую объект QueryBuiler.
Добавлять это всё в Query идея так себе. В конечном итоге у вас разрастется OrderListFilter
Раз вы говорите эти фразы, значит не представляете. Список полей в OrderListFilter зависит только от количества полей в форме в UI. Новые поля там появляются только если попросил бизнес, а не потому что программист так захотел, поэтому никуда он не разрастется. OrderListFilter это DTO на бэкенде, которое является моделью формы ввода на фронтенде, он приходит из контроллера и в идеале создается и заполняется автоматически фреймворком. OrderListFilter не создается программистом в коде других методов, и метод list() не используется программистом в другой логике для получения списка заказов. Если нужно получить список заказов, программист настраивает query builder.
Тогда какой смысл в OrderService? Прослойка перед репозиторием с двумя методами?
OrderService содержит логику бизнес-действий. Количество методов там соответствует количеству бизнес-действий с сущностью. При этом количество сервисов может быть больше одного - один для пользовательской части, один для админки, один для сообщений из очереди, с разным набором методов. OrderListFilter для пользовательской части содержит одни поля, для админской другие.
OrderListFilter только для случаев где реально требуется пагинация и фильтрация.
Да, я именно так и написал "принимает DTO с фильтром из интерфейса". Метод list() из этого примера возвращает данные для страницы списка заказов например в админке, с фильтром и пагинацией. Для списка заказов в личном кабинете пользователя будет другой сервис со своим DTO, где будут другие поля, и метод list() будет также принимать текущего пользователя, чтобы добавить его id в query builder.
Все остальные запросы в различных сервисах и джобах это отдельные методы в репозитории.
Вот я и объясняю, что у них есть недостатки. Обычно в каждом таком месте нужны свои условия, поэтому удобнее использовать спецификацию.
Я писал код и с использование 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;
}
}
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. Так все элементы имеют свою ответственность - репозиторий представляет коллекцию, сервис содержит логику фильтров, сущность ничего не знает про списки сущностей, и бизнес-понятия не разбросаны по всему коду.
Просто при любой работе с Позициями держать в голове необходимость пересчитать Скидку. Просто быть идеальным 24/7, что может быть проще? Или вы можете применить паттерн агрегат по его прямому назначению и оставить возможность менять коллекцию Позиций только через его интерфейс
Подмена понятий и логическая манипуляция. Если эта логика будет в агрегате, то "любая работа с Позициями" будет в "его интерфейсе", и точно так же при создании или изменении любого метода агрегата, который работает с позициями, надо "держать в голове необходимость пересчитать Скидку", то есть "быть идеальным 24/7".
Почему-то многие сторонники DDD не понимают, или делают вид, что хоть с логикой в сущности, хоть не в сущности, количество методов, которые ее изменяют, будет одинаковым. Потому что оно идет из бизнес-требований.
"Вероятность в 100" это и есть доказательство. Получить ее относительно реальных явлений можно только экспериментом. Без этого всегда есть вероятность, что мы чего-то не знаем, и рассуждения дают вывод, не соответствующий реальности. Пример - разные теории о строении атома. "Вероятность в 0" это опровержение, его можно получить контр-примером, но это тоже эксперимент, и тоже всегда есть вероятность, что мы чего-то не учитываем.
исполнение этого метода по смыслу требует обработки некоего побочного эффекта. Это может быть обращение к внешнему сервису, отправка email, запуск другого сценария с использованием другого агрегата составить общий список накопленных доменных событий, после чего обработать их, а уже затем подтверждать транзакцию.
Угу, и обращение к внешнему сервису произойдет до того, как сущность сохранена. Туда отправятся данные вместе с id сущности, он сохранит их в свою базу, потом решит запросить дополнительную информацию по id, а у нас такой сущности нет, потому что при обработке следующего доменного события произошла ошибка, и транзакция откатилась. Сначала оплачиваем заказ, потом сохраняем информацию, что он оплачен, а уже потом рассказываем всем интересующимся, что он оплачен.
Вообще-то функция проверки наличия файла файловой системы должна возвращать boolean, а не бросать исключение. Поэтому "логично" сделать if, а не try/catch. Это как раз то, о чем говорит автор, только он слишком преувеличивает.
Аналогично, функция валидации входных данных должна возвращать boolean и список ошибок, а не бросать ValidationException. Обработка некорректного ввода должна быть предусмотрена, поэтому это не исключительная ситуация. А вот контроллер уже может бросить ValidationFailedHttpException, если это требуется фреймворком, а может и не бросать, а использовать метод контроллера return this.sendValidationErrorResponse(validationResult).
COPY имеет смысл использовать как разовую загрузку очень большого количества данных, которые не нужно проверять. Если нужно проверять, то с COPY придется делать это триггерами, то есть логика будет в базе со всеми ее недостатками. Если COPY регулярно используется в приложении, значит объемы данных не очень большие, проще загружать обычным способом через приложение в транзакции.
Проверял скорость для большой таблицы, 60 колонок, 20+ триггеров. С COPY 10000 записей грузятся 10 секунд, с логикой в приложении без триггеров 4.8 секунды, сохранение в одной транзакции массовыми INSERT по 100 строк. Проверки, аналогичные триггерам, для 10000 записей в приложении занимают 100мс.
Предполагаю, что автор. Спасибо.
Иными словами, "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".
Если вы прочитаете внимательно ветку обсуждения, то увидите, что я приводил цитату "Transactions should not cross aggregate boundaries" как возражение на ваше утверждение "никакие границы тут не нарушаются, да и с чего бы вдруг?". Вот я вам и объяснил, с чего вдруг. Выражение "should not" на русский язык переводится как "не следует", прямой запрет придумали вы сами.
Вам уже несколько человек объяснили, что это трактовка самих авторов DDD. Они рекомендуют сохранять каждый агрегат в отдельной транзакции, называют это запланированным использованием агрегатов.
1. В этой ветке я говорю только про ваше утверждение о границах транзакций.
2. В других ветках я приводил возражения не на DDD в целом, а конкретно на ваши утверждения из статьи.
3. Работающую альтернативу я предложил в своей статье. Там есть пример бизнес-требований на 6 небольших действий и работающий код с их реализацией. Если хотите сравнить, какая альтернатива более работающая, пишите свою версию в том стиле, который вы считаете DDD, сравним. Так как есть готовый код, это должно занять у вас пару часов. Обсудим, в каких ситуациях те или иные решения дадут преимущество, а в каких не дадут.
Но я подозреваю, что вы сольетесь, как и другие сторонники DDD, как только доходит до дела.
Ну и там дальше это написано прямым текстом:
"Just understand that this is not the primary way that Aggregates are meant to be used"
"Use eventual consistency for all other [aggregates]" как раз и означает "Aggregate в DDD это граница транзакционности".
Приведите пожалуйста цитату из сторонних источников с вашим пониманием. Если не приведете, значит странное понимание у вас, а не у других.
Удивляют меня люди, которые без всяких причин уверены, что их понимание единственно верное, и не могут погуглить прежде чем спорить об этом.
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 считается, что нет. Только в данном случае он просто ссылается на то, что говорили другие.
https://martinfowler.com/bliki/DDD_Aggregate.html
Transactions should not cross aggregate boundaries.
Тут нет высказывания о реальных явлениях. Если считать высказыванием "Во Вселенной есть такой чертеж", то да, вероятность его в 100% можно получить только наблюдением такого чертежа (то есть экспериментом).
Но в данном случае его вероятность 0%, так как выражение "круглого квадрата" создает логическое противоречие, и это высказывание не имеет смысла. Оно нефальсифицируемое, его нельзя подтвердить или опровергнуть, потому что непонятно, что имеется в виду. Ну или мы можем сказать, что тут ничего не имеется в виду.
Кроме того, это высказывание неявно подразумевает, что квадраты могут быть круглыми, поэтому контр-примером можно считать факт, что квадраты не называются круглыми.
Естественно, поэтому ваше ерничание неуместно.
В таком случае класс, который содержит фильтры по ним, является частью домена, и нарушения слоев вообще нет.
Он не отдельный сервис, я же написал, что говорю про случай, когда логика находится в сервисах. То есть они в любом случае есть, они не сделаны специально для работы с репозиторием, поэтому они не "отдельные". Они содержат всю логику бизнес-действий, неважно, репозитории там используются или что-то еще. Вместо репозитория может быть SaaS сервис с GraphQL. Конвертирование OrderListFilter в DTO для GraphQL-запроса это аналог конвертирования OrderListFilter в QueryBuilder для репозитория. Этот код где-то должен быть, и удобно помещать его в сервис, не в контроллере же его писать. А сущностей с SaaS-сервисом у нас нет.
Не означает. В админке есть действия "показать список товаров, создать товар, изменить товар, удалить товар", сервис ProductService содержит логику всех этих действий. С OrderService аналогично, только там другие действия.
QueryBuilder это более высокоуровневая абстракция, чем конкатенация строк с SQL. Вы же в курсе, что уровней абстракции много, а не только 2 "высокий" и "низкий"?
Класс спецификации это по определению уровень домена. QueryBuilder от нее ничем не отличается, кроме того, что он в другом неймспейсе, и там вместо условия по бизнес-свойству
andWhere('=', 'isVip', true)
будут более детальные условия по полям таблицыandWhere('in', 'status', [Status.Platinum, Status.Diamond])
. Делать ли отдельный класс ради чистоты слоев дело ваше, но обычно QueryBuilder достаточно.В данном случае он играет роль спецификации, поэтому всё нормально. Это соответствует тому, что пишет Фаулер: "Client objects construct query specifications".
QueryBuiler сам по себе достаточно высокоуровневая абстракция, по нему можно построить и HTTP-запрос к стороннему API, а не только SQL. Вы можете сделать отдельный класс спецификации, но он будет работать так же, как QueryBuiler, с методами andWhere и т.д.
В третий раз объясняю - OrderListFilter не создается программистом в коде других методов, и метод list() не используется программистом в другой логике для получения списка заказов.
Единственное место, где он создается - API endpoint для получения списка заказов для соответствующей страницы на фронтенде. Этот DTO принимает поля, которые отправляет фронтенд. Если есть другая страница со списком заказов с другим набором полей в фильтре, для нее будет другой endpoint и другой DTO.
Вместо findSomethingBySomething я формирую объект QueryBuiler.
Раз вы говорите эти фразы, значит не представляете.
Список полей в OrderListFilter зависит только от количества полей в форме в UI. Новые поля там появляются только если попросил бизнес, а не потому что программист так захотел, поэтому никуда он не разрастется. OrderListFilter это DTO на бэкенде, которое является моделью формы ввода на фронтенде, он приходит из контроллера и в идеале создается и заполняется автоматически фреймворком. OrderListFilter не создается программистом в коде других методов, и метод list() не используется программистом в другой логике для получения списка заказов. Если нужно получить список заказов, программист настраивает query builder.
OrderService содержит логику бизнес-действий. Количество методов там соответствует количеству бизнес-действий с сущностью. При этом количество сервисов может быть больше одного - один для пользовательской части, один для админки, один для сообщений из очереди, с разным набором методов. OrderListFilter для пользовательской части содержит одни поля, для админской другие.
Да, я именно так и написал "принимает DTO с фильтром из интерфейса". Метод list() из этого примера возвращает данные для страницы списка заказов например в админке, с фильтром и пагинацией. Для списка заказов в личном кабинете пользователя будет другой сервис со своим DTO, где будут другие поля, и метод list() будет также принимать текущего пользователя, чтобы добавить его id в query builder.
Вот я и объясняю, что у них есть недостатки. Обычно в каждом таком месте нужны свои условия, поэтому удобнее использовать спецификацию.
Вы неправильно представляете, как это работает findByQuery принимает не DTO, а QueryBuilder из ORM или более высокоуровневую спецификацию, которая работает аналогично. Поэтому добавлять туда ничего не надо, и никакой проблемы с датами нет. Сервис принимает DTO с полями createdAtFrom, createdAtTo, настраивает QueryBuilder, передает в репозиторий. Делать универсальный класс со всеми возможными полями для фильтров точно не нужно, как раз из-за тех проблем, которые вы описали.
Именно метод сокрытия таких механизмов. Не всех, а некоторых.
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.
При том, что фильтры, сортировки и страницы обсуждаются в бизнес-требованиях. Стандартный пример - галочка в фильтре "Только товары с изображениями".
Непонятно, на кой пень нам тогда репозитории, если мы будем всегда получать данные через какой-то другой handler.
Нет, это означает, что есть десятки комбинаций нескольких доменных концепций. И потенциально таких комбинаций очень много.
Доменные концепции это часть бизнес-логики, в репозитории не должно быть бизнес-логики.
Репозиторий, как абстракция коллекции, для выборки данных должен иметь методы findById и findByQuery. Это аналог методов прямого доступа по индексу и поиска элементов по условию для обычной коллекции.
Аргументом для findByQuery можно передавать специальный объект спецификации или просто настроенный QueryBuiler из ORM.
Для методов вида findSomethingBySomethingAndSomethingAndSomething() обычно получается так, что их становится много, а используются они только в одном месте кода. К тому же есть вопросы сортировки и пагинации. Их удобно мокать в тестах, но в остальном они создают больше сложностей, чем решают, поэтому лучше их не делать.
Это удобно ложится на логику в сервисах. В сервисе есть метод list(), он принимает DTO с фильтром из интерфейса и настройками сортировки и пагинации, настраивает по ним QueryBuilder, передает в репозиторий, получает список сущностей. Для пагинации нужно общее количество, можно в репозитории сделать метод findByQueryWithTotal или отдельно findTotalForQuery. Так все элементы имеют свою ответственность - репозиторий представляет коллекцию, сервис содержит логику фильтров, сущность ничего не знает про списки сущностей, и бизнес-понятия не разбросаны по всему коду.
Подмена понятий и логическая манипуляция.
Если эта логика будет в агрегате, то "любая работа с Позициями" будет в "его интерфейсе", и точно так же при создании или изменении любого метода агрегата, который работает с позициями, надо "держать в голове необходимость пересчитать Скидку", то есть "быть идеальным 24/7".
Почему-то многие сторонники DDD не понимают, или делают вид, что хоть с логикой в сущности, хоть не в сущности, количество методов, которые ее изменяют, будет одинаковым. Потому что оно идет из бизнес-требований.
Архитектор не должен делать то, архитектор не должен делать это... А что вообще он должен делать?
Что конкретно это означает? У вас допустим есть десяток senior-разработчиков с опытом 10+ лет, почему их решения недостаточно осмысленные?
"Вероятность в 100" это и есть доказательство. Получить ее относительно реальных явлений можно только экспериментом. Без этого всегда есть вероятность, что мы чего-то не знаем, и рассуждения дают вывод, не соответствующий реальности. Пример - разные теории о строении атома.
"Вероятность в 0" это опровержение, его можно получить контр-примером, но это тоже эксперимент, и тоже всегда есть вероятность, что мы чего-то не учитываем.
Угу, и обращение к внешнему сервису произойдет до того, как сущность сохранена. Туда отправятся данные вместе с id сущности, он сохранит их в свою базу, потом решит запросить дополнительную информацию по id, а у нас такой сущности нет, потому что при обработке следующего доменного события произошла ошибка, и транзакция откатилась. Сначала оплачиваем заказ, потом сохраняем информацию, что он оплачен, а уже потом рассказываем всем интересующимся, что он оплачен.
Вообще-то функция проверки наличия файла файловой системы должна возвращать boolean, а не бросать исключение. Поэтому "логично" сделать if, а не try/catch. Это как раз то, о чем говорит автор, только он слишком преувеличивает.
Аналогично, функция валидации входных данных должна возвращать boolean и список ошибок, а не бросать ValidationException. Обработка некорректного ввода должна быть предусмотрена, поэтому это не исключительная ситуация. А вот контроллер уже может бросить ValidationFailedHttpException, если это требуется фреймворком, а может и не бросать, а использовать метод контроллера
return this.sendValidationErrorResponse(validationResult)
.