Комментарии 58
Очевидным решением будет применение Принципа Единственной Ответственности. Который прямо обязывает нас следить за тем, чтобы один компонент отвечал за что-то своё. Мы напишем две функции — для отдела кадров и бухгалтерии, — каждая из которых будет жить своей жизнью и отвечать перед кем-то одним.
В вашем примере функция считала сверхурочное время сотрудника. Каким образом можно было ранее предвидеть, что это время может считаться различным образом с точки зрения отдела кадров и с т.з. бухгалтерии?
Помимо "один компонент отвечает за что-то своё", в S входит тезис "что-то свое должно быть полностью инкапсулировано в компонент". Применяя к вашему примеру, почему ответственность за расчет сверхурочного времени должна быть размыта между разными компонентами?
Я думаю это был не удачный пример. Функция изначально вовсе и не нарушала Приницип Единственной Ответственности.
Про (1). Конечно нельзя заранее предугадать, что расчёты будут разные. Поэтому изначальное решение не ошибка, а гипотеза разработчика, что бухгалтерия и отдел кадров - одно действующее лицо (ДЛ, actor), причём решение почти точно согласованное с бизнесом. Со временем требования меняются и вполне допустимо, что ДЛ разбивается на два. Тут главная мысль в том, что нужно уловить момент, когда идёт разбиение на два ДЛ, а когда нет (что не всегда можно сделать).
Принцип Единой Ответственности плох тем, что он операется на зыбкое, точно не определеннное понятие "ответственность". Можно к примеру, сделать объект "клиент", который отвечает за все операции, касающиеся клиента - и вот вам "божественный объект", который формально SRP соответствует, но запутанность (coupling) внутри приложения создает ого-го какую! Причем, эффект от этой запутанности может быть разным: в маленьком приложении ей можно принебречь, а вот в большом, расползшемся на разные области с разными взавимодействиями с клиентом, все прелести хрупкости/жескости/неподвижности встанут в полный рост.
И то, что ответственность каждый может понимать по-своему - это ещё пол-беды. Беда в том, что правильное, т.е. адекватное задаче приложения, понимание меняется вместе с развитием приложения. Как здесь: пришла новая нормативка по подсчету - и единая ответственность внезапно разделилась на две. Придет другая нормативка - например, по разнице подсчета средней продолжительности рабочего месяца в невисокосном и високосном годах - ответсвенность ещё поделится на две, и вот их уже четыре.
То есть, сам принцип SRP требует для своего применения творческого (и я сказал бы - диалектического, см. ниже) подхода.
Конечно, кто-нибудь (например, автор статьи) может мне сказать, что предметно-ориентированныое проектирование спасет от этой беды, но ведь клиент-то реально один, и как его поделить на ограниченные контексты - это вопрос именно что диалектический, потому как упирается в эту самую диалектику как учение о всеобщей связи и зависимости, которой наше поколение некогда пытались научить (получалось, правда, обычно плохо).
То есть вся эта наука, про которую тут пишет автор статьи - это субстанция зыбкая, требует не только конкретизации по месту, но и предвидения, куда будет развиваться конкретная проектируемая система. То есть, сама по себе она фундаментом служить не может, а требует для своего применения людей, которые будут применять ее творчески в конкретных условиях и пользуясь своим невербализуемым опытом. Что эта наука может дать - так, во-первых, общий язык, а во-вторых - эврстики, которыми можно пользоваться для выбора решения. Но можно и не пользоваться - правильный выбор, по-любому, остается за человеком, творцом.
И из этого есть ещё кое-какие следствия, но об этом - как нибудь, в другой раз (статью, что ли написать :-) ).
Одна из ключевых проблем SOLID в том, что все забывают зачем он нужен - для создания систем, которые будут динамически развиваться и поддерживаться.
SRP влияет на разные уровни абстракции - реализации метода, компоновки поведения (класс и объект), сервисные сценарии, приложения (монолиты, микросервисы), системные архитектуры и их компоненты.
Когда мы используем SOLID, мы создаем компоновку кода такой, чтобы она умела реагировать на изменение событий. И в данном случае уровень влияния SRP должен сместится выше - к объекту, а изменение должно соответствовать при этом буквально следующей литере в акрониме - OCP.
Сам по себе SRP вполне себе ясный инструмент акронима, беда в том, что вместо понимания и восприятия идей, начинается догматизм в попытке выяснить, что значит слово Responsibility. В то время как динамические идеи вообще плохо догматизируются, они нужны как инструмент мышления и оценки.
Задача SOLID в целом, позволить сформировать дизайн, который будет эволюционировать по ходу существования системы, а не дать ответы на все вопросы в камне прямо сейчас. И SRP один из примеров - т.к. Мартин определял его на протяжении 20 лет по разному для тех, кто пытался молиться на акроним. Это бесполезно, суть сборника идей в их адаптации, а не в побуквенной диалектике.
Ну Мартин объясняет это тем, что многие неправильно понимают этот принцип. Он говорит, что у компонента должна быть только 1 причина для изменения. Если функция используется двумя разными отделами, то требования для изменения будут приходить из 2х источников. Значит принцип единственной ответственности нарушается.
Если функция используется двумя разными отделами, то требования для изменения будут приходить из 2х источников.
По вашим словам получается, что понятие "ответственность" лежит за пределами свойств собственно ПО, а определяется организационными и бюрократическими причинами. Я вас правильно понял?
Если так, то там опять-таки возникает неопределенность в понимании, где именно границы ответственности, и опять-таки границы могут поменяться в процессе эксплуатации.
По вашим словам получается, что понятие "ответственность" лежит за пределами свойств собственно ПО, а определяется организационными и бюрократическими причинами. Я вас правильно понял?
Именно так Мартин это и объясняет. Речь не про ответственность класса (модуля), а про ответственность за класс (модуля). В пределах ПО для этого и так хватает принципов - low coupling и high cohesion, например.
где именно границы ответственности
Там же, где и должностные, наверное.
границы могут поменяться в процессе эксплуатации
И это нормальный естественный процесс. ПО тоже меняется.
Спасибо. Очень качественно изложено. Похоже DDD, не только методология качественного проектирования, но и хорошо работает для построения любых сложных рассуждений.
А будет какое-то обоснование "сервисы должны взаимодействовать только через интерфейсы"? Что-то кроме "так правильно"? Бесполезный Heder Interface просто повторяющий паблик-методы это же так чистокодно!
Ну завязка на реализацию не есть хорошо. Вдруг потом захочется подменить и придётся всё переписывать.
Хотя конечно понимаю о чём вы, чаще всего это будет излишней преждевременной оптимизацией
Наконец-то, а продолжение будет? Покажите код. Есть что нибудь сложнее хеллоу ворлд?
Особенно интересуют всякие приколюхи типа подгрузки данных из хранилища в процессе работы или текучие абстракции, когда доменная модель через инверсию зависимости начинает знать о репозитории…
Бизнес-сущности ничего не умеют делать и ничего не знают. Они полностью беспомощны. Они просто существуют, и всё. Их единственная задача — наполнять своим существованием предметную область приложения.
Как мы выяснили ранее, бизнес-сущности сами по себе ничего не умеют. Умения бизнес-сущностей сконцентрированы в сервисах.
Вы описали антипаттерн который противоречит основным принципам ООП и тактическим паттернам DDD описанных в книгах Еванса и Вернона. У Мартина Фаулера есть даже отдельная статья на эту тему
Вот цитата из этой статьи:
In general, the more behavior you find in the services, the more likely you are to be robbing yourself of the benefits of a domain model. If all your logic is in services, you've robbed yourself blind.
Полностью можно ознакомиться тут:
Этот Агрегат будет содержать в себе всё, что касается товара, например:
свойства Товара;
корзины, в которых добавлен Товар;
историю изменения стоимости Товара, и так далее.
Да вы что, Товар и Корзина это совершенно разные агрегаты. Товар может существовать без корзины, если на сайте нет функционала покупок, а корзина может работать только с id товаров, а не с объектами.
Пример агрегата - это агрегат Order, который состоит из сущностей Order и OrderItem. Существование OrderItem без Order не имеет смысла.
В агрегат Product могут например входить сущности Product и ProductImage.
В агрегат Cart - сущности Cart и CartLine.
В DDD предполагается, что агрегат должен загружаться из хранилища целиком, то есть нельзя загрузить отдельный OrderItem, надо загружать Order и через него управлять отдельным OrderItem. А корзину и товар можно загружать по отдельности.
Для нашего Агрегата «Товар» таким Объектом-значением может являться История изменения стоимости.
История изменений это не "value object", она состоит из отдельных записей, которые в контексте бизнеса обычно являются сущностями.
Value object это просто составное значение. Типичный пример value object - это Money { value: number, currency: string }
. Или например дробное значение можно хранить в виде значения типа float 1.234
, в виде строкового значения "1.234"
, а можно в виде объекта {"integer": 1, "fractional": 234}
. Вот этот объект и называется value object.
Сервис — это класс, который реализует бизнес-логику.
Ну как бы по DDD бизнес-логика должна быть в сущностях, а не в сервисах.
Так же, как и в предметно-ориентированном проектировании, предметная область является центральным элементом структуры приложения. Только называется по-другому: бизнес-сущности.
Бизнес-сущности
Бизнес-сущности ничего не умеют делать и ничего не знают. Они полностью беспомощны. Они просто существуют, и всё. Их единственная задача — наполнять своим существованием предметную область приложения.
И именно они являются той самой предметной областью.
Воняет Data Class-ом, аж не продохнуть!
Объекты, которые ничего не умеют делать, — это не объекты. "Или крестик снимите, или трусы наденьте" — либо делайте нормальные живые объекты, либо удаляйте отсылки к ООП, когда про процедурное программирование рассказываете.
Оказывается, про это уже сказали.
Писец, хочу как бы напомнить что плодить сущности без разбора тоже не есть хорошо. Т.к. современные компы плохо в параллельность
Прокомментирую раздел статьи, который касается принципов предметно-ориентированного проектирования на внутрисервисном уровне.
Насколько представляю себе из литературы по ddd агрегат как раз должен в себе содержать функционал, связанный с работой его внутренней бизнес-логики. И выделять отдельно сервис для обработки данных отдельного агрегата надо только исходя из каких-то специфических требований. А в общем случае доменные сервисы нужны, если требуется одновременная обработка данных для двух и более агрегатов. Но есть другая проблема про которую не встречал упоминания в литературе, но она не так уже и редка и в моих проектах встречалась многократно. Например внутри агрегата идёт расчёт сложного алгоритма - типовой пример расчёт теплообменника. В зависимости от того по какой ветке пошёл расчёт, алгоритму требуются специфические (именно для этой ветки алгоритма) справочные данные из базы данных. И приходится агрегат подключать к слою persistence layer для получения данных из бд. Агрегат получает зависимость от другого слоя приложения. Но это противоречит определению агрегата доменной модели, как автономной сущности не зависящей от внешнего окружения домена. На текущий момент времени мне не удалось найти описания или хотя бы обсуждения решения подобной проблемы. Под проблемой понимаю внесение в агрегат зависимости от внешнего окружения домена.
Можно эту зависимость инвертировать, вот тут описан подход который мы используем https://habr.com/ru/articles/799019/
В зависимости от того по какой ветке пошёл расчёт, алгоритму требуются специфические (именно для этой ветки алгоритма) справочные данные из базы данных. И приходится агрегат подключать к слою persistence layer для получения данных из бд. Агрегат получает зависимость от другого слоя приложения. Но это противоречит определению агрегата доменной модели, как автономной сущности не зависящей от внешнего окружения домена.
Дык, не надо натягивать совуподход к проектирования (точнее даже, свое понимание его) на глобусзадачу, которую для которой он подходит плохо. Следуйте совету великого русского писателя Козьмы Пруткова "Зри в корень!".
Если зрить в корень, выделить главное, основное, то агрегат - это набор сущностей, которые должны меняються согласованно, чтобы сохранялись инварианты предметной области, и этим его выделение полезно. Все остальное - выделение корня, изоляция с разрешением доступа через корень - это уже методы гарантировать такой порядок изменения. Вы задайте себе вопрос - должны ли справочники меняться вместе с сущностями агрегата? И вообще - должны ли они меняться, кроме как пополняться новым содержимым и делать неактуальными часть старого (которое тем не менее остается доступным для ссылки из уже разработанных и ссылающихся на него проектов)? Нет? Тогда справочники меняются независимо, если вообще меняются, и нечего справочникам в агрегате делать. Куда и как тянуть связь к ним - решайте по задаче сами, не оглядываясь ни на какой подход проектиктирования: он неизбежно ограничен, ибо жизнь всегда богаче любой теории.
Впрочем, мне смутно припоминается, что в DDD есть способы описания и для такой задачи: на ум приходит что-нибудь типа сервисов предметной области или вообще интерфейсов к другому ограниченному контексту. Но я не теоретик, а потому точно не скажу.
На текущий момент времени мне не удалось найти описания или хотя бы обсуждения решения подобной проблемы.
Решение есть, вполне себе простое - не использовать логику в сущностях, а использовать логику в сервисах. Обсуждения этого встречаются довольно часто. Проблемы с логикой в сущностях начинаются как раз из-за внедрения зависимостей.
В рамках DDD нормального решения нет, есть разные обходные пути, один из них это передавать все зависимости в аргументах метода. При этом используется такой самообман, что типа интерфейс зависимости принадлежит домену, а реализация техническому слою, поэтому передавать интерфейс в сущность это нормально. Хотя их использование в коде метода выглядит одинаково.
При этом используется такой самообман, что типа интерфейс зависимости принадлежит домену, а реализация техническому слою,
У этого "самообмана" даже официальное название есть Dependency inversion principle :)
https://en.m.wikipedia.org/wiki/Dependency_inversion_principle
Да неважно, есть ли у него название) Слой определяется функциональностью, а функциональность выражается в названии. У реализации и интерфейса одинаковые названия методов, поэтому интерфейс находится в том же слое, что реализация. Если вы передаете в метод сущности аргументы Balance или Product, и вместе с ними SomeRepositoryInterface, то получается смешивание уровней абстракции, потому что понятия Balance и Product в бизнес-требованиях есть, а понятия SomeRepository там нет.
Вы не поверите, но для решения этой проблемы тоже придумали "самообман" https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)#Pure_fabrication
И между прочим ваши доменные сервисы и есть одной из реализаций этого паттерна :)
Если под решением вы подразумеваете "не передавать зависимости в сущность", то я так сразу и сказал.
Нет, я имел ввиду что в коде доменной области у вас никак не получится избавится от объектов, которые не имеют непосредственного отражения в предметной области, и ваш "доменный сервис" ничуть не лучше "репозитория" либо любой другой "Pure Fabrication" которая облегчает имплементацию доменной логики.
А вот как раз лучше. Когда мы передаем репозиторий в сущность, мы смешиваем бизнес-логику с деталями реализации. А с сервисом мы передаем все технические зависимости в конструктор, и в методах приходят только аргументы, которые представляют что-то из бизнес-логики. В сущности тоже остаются только свойства, которые мы выделили при анализе предметной области. Код метода в сервисе содержит детали реализации, это точка соединения разных уровней абстракции, там нормально обращаться к техническим зависимостям, потому что из этого как раз реализация и состоит. Технические зависимости лежат в свойствах сервиса, и бизнес-терминов там нет. Всё красиво сгруппировано.
Сам сервис с логикой, как ни странно, тоже имеет отражение в предметной области. Это инструкция. В бизнес-требованиях есть инструкция как что-то делать - как создать заказ, как рассчитать теплообменник. Метод сервиса это модель этой инструкции, код метода должен содержать шаги из инструкции. Если поменялись требования, находим соответствующий метод и меняем нужные шаги. Не надо переписывать половину приложения потому что после новых требований оказалось, что мы поместили логику не в ту сущность.
Мне кажется что мы заходим на очередной круг, когда я вам пытаюсь объяснить преимущество ООП перед процедурным программированием. Мне не удалось это сделать в дискуссиях под другими статьями, видимо не удастся и здесь, Если процедурный подход работает для систем которые вы строите, используйте его дальше. Возможно, когда/если появятся проблемы, вы еще раз посмотрите в сторону ООП и поймете преимущества подхода, когда объект сам контролирует свои инварианты.
Так вот в том и дело, что я вам пытаюсь объяснить, что это вы неправильно представляете, что такое ООП :)
Да, методы, изменяющие данные, должны находиться в одном классе с данными, но эти методы должны работать исключительно с полями класса и ни с чем другим. Если вам нужны какие-то внешние зависимости, значит этот метод не должен находится в классе. Методы могут принимать данные в аргументах, но вам нужен код, который будет подготавливать эти данные, и он должен быть вне сущности.
Условно говоря, вы пытаетесь запихнуть в драйвер файловой системы функции записи видео, потому что запись видео меняет данные файла и содержит какие-то ограничения на них (инварианты). Но это неправильно, система записи видео должна снаружи управлять системой записи данных в файл, в этом нет никакой проблемы.
Процедурное программирование как раз отличается от программирования с объектами. Потому что у объектов есть конструктор. Именно поэтому это ООП.
Возьмем такой пример.
Пример
class EntityService
{
public function __construct(
EntityRepository $entityRepository,
DateService $dateService,
Logger $logger,
) {}
public function createEntity(CreateEntityDto $dto) {
$this->logger->info('Create entity');
$entity = new Entity();
$entity->property1 = $dto->property1;
$entity->property2 = $dto->property2;
$entity->createdAt = $this->dateService->getCurrentDate();
$entity->updatedAt = $entity->createdAt;
$this->logger->info('Entity initialized');
$this->entityRepository->save($entity);
$this->logger->info('Entity saved');
}
public function updateEntity(Entity $entity, UpdateEntityDto $dto) {
$this->logger->info('Update entity');
$entity->property1 = $dto->property1;
$entity->property2 = $dto->property2;
$entity->updatedAt = $this->dateService->getCurrentDate();
$this->logger->info('Entity initialized');
$this->entityRepository->save($entity);
$this->logger->info('Entity saved');
}
}
Как это будет выглядеть с процедурным программированием?
function createEntity(
CreateEntityDto $dto,
EntityRepository $entityRepository,
DateService $dateService,
Logger $logger,
) {
...
}
function updateEntity(
Entity $entity,
UpdateEntityDto $dto,
EntityRepository $entityRepository,
DateService $dateService,
Logger $logger,
) {
...
}
Зависимости надо будет передавать в каждую процедуру, либо использовать глобальные переменные. Что и создает основные недостатки.
Как это будет выглядеть с логикой в сущностях?
class Entity {
private $property1;
private $property2;
function __contruct(
CreateEntityDto $dto,
DateService $dateService,
Logger $logger,
) {
$logger->info('Create entity');
$this->property1 = $dto->property1;
$this->property2 = $dto->property2;
$this->createdAt = $dateService->getCurrentDate();
$this->updatedAt = $this->createdAt;
$logger->info('Entity initialized');
// сохранение находится где-то еще
}
function update(
UpdateEntityDto $dto,
DateService $dateService,
Logger $logger,
) {
$logger->info('Update entity');
$this->property1 = $dto->property1;
$this->property2 = $dto->property2;
$this->updatedAt = $dateService->getCurrentDate();
$logger->info('Entity initialized');
// сохранение находится где-то еще
}
}
Какой пример больше похож на процедурное программирование - код EntityService или код Entity?)
Можно передавать зависимости всех методов в конструктор сущности, но тогда всё будет еще больше непонятно - какие используются только в конструкторе, какие в других методах, какие являются бизнес-свойствами сущности.
А еще бывает, что есть бизнес-требования к вызовам стороннего API в виде мутаций GraphQL. То есть даже в вашем коде даже сущностей не будет. Где вы будете писать бизнес-логику?
Я вам уже показывал примеры нормальных доменных объектов с поведением в дискуссиях под другими статьями. Повторять одни и те же аргументы еще раз я не вижу смысла, вы их не слышите. Если вы считаете что наличие класса с конструктором делает ваш код объектно-ориентированным, то у нас с вами представление о ООП очень сильно отличаются, и мы все равно друг друга не поймем.
Объектно-ориентированным код делает наличие объектов. Всё. Именно поэтому оно так называется.
Представления у нас в целом одинаковые, просто вы неправильно представляете критерии, по которым определяется принадлежность методов объекту - что является деталями реализации, что бизнес-логикой, к какому объекту относится инвариант.
Попробую еще раз объяснить. Вот вы говорите про инварианты. Инвариант самой сущности может относится только к деталям реализации ее свойств. Вот есть у вас сущность, где вы храните все свойства в одном поле $data
, в методах вы можете например контролировать, что в нем не появится новых свойств. Но сущность не может контролировать бизнес-требования, потому что она их не задает, они задаются бизнесом и существуют отдельно от сущности, сущности появляются при их анализе. Бизнес-требования первичны, а сущности вторичны.
Кстати, подумайте, как будет выглядеть бизнес-логика с полем $data
. Тут наглядно видно, как смешиваются разные уровни абстракции.
Инварианты, связанные с бизнес-требованиями, не принадлежат сущности, хотя бы потому что могут быть бизнес-требования, в которых инвариант затрагивает несколько сущностей. "У пользователя не может быть более 3 статей на модерации". Это надо проверять при отправке статьи на модерацию, то есть в вашем подходе этот код должен быть в сущности Article. Но это не инвариант одной этой Article. При этом для соблюдения логического инварианта тут еще нужны вполне технические мьютексы на процесс "Отправка на модерацию пользователем N", которые должны освобождаться только после сохранения в базу. То есть либо вам надо перенести сохранение в базу и работу с мьютексами в сущность, либо часть логики соблюдения инварианта будет вне сущности.
и мы все равно друг друга не поймем.
Конечно, если игнорировать неудобные вопросы, то и понять будет нельзя. Еще раз предлагаю вам подумать над вопросом, где вы будете соблюдать инварианты в случае бизнес-требований к вызовам GraphQL API.
Попробую еще раз объяснить.
Не трудитесь. Все это мы уже с вами не раз обсуждали в других ветках, с примерами кода. Но у вас есть свое, очень специфическое понимание ООП которое мне, к сожалению, никогда не понять.
Я уже объяснил, почему не понять. Потому что когда доходит до дела, вы начинаете игнорировать неудобные вопросы и уходить от ответа. Если так не делать, то понять будет несложно.
Раз вы второй раз ссылаетесь на дискуссии, то прокомментирую. Дискуссия ранее у нас была только одна, в моей статье про логику в сервисах. Вы там привели 4 примера кода.
В 1 мы обсуждали абстрактный вопрос что такое поведение объекта, он не показывает достоинства и недостатки обсуждаемых подходов.
В 2 вы использовали некий Outside, которого нет в бизнес-требованиях. Я указал, что это не соответствует Ubiquitous language и уменьшает понятность кода.
В 3 вы использовали Transactional Outbox, на что я указал, что его можно использовать и в моем подходе.
В 4 вы написали какой-то посторонний метод на добавление продукта, который удобен вам и которого в бизнес-требованиях не было. Это единственный пример, который напрямую относится к основной теме обсуждения.
Обсуждения закончились вашими фразами:
"Не вижу смысла обсуждать такие тривиальные вещи как слой UI"
"Если для вас все это является критическими недостатками, используйте свой подход."
"Ок, вы не понимаете преимущества ограниченных контекстов и похоже мне не удастся до вас эту информацию донести"
Мое предложение в статье и в комментариях написать код приложения по указанным бизнес-требованиям и сравнить вы проигнорировали. Видимо потому что понимаете, что код в вашем подходе будет выглядеть сложнее.
Пока просматривается одно решение - алгоритм реализовать в доменном сервисе, а агрегат использовать, как хранилище исходных и расчётных данных алгоритма. И тогда обращение к базе данных за очередной порцией справочных данных будет идти из доменного сервиса. В этом случае агрегат может остаться "вещью в себе" и не содержать в себе никаких зависимостей от внешней инфраструктуры.
Мне не совсем понятно почему внешнюю зависимость нельзя внедрить в агрегат, но можно внедрить в доменный сервис, который находиться в том же слое. Вместо этого предлагается откатится к анемичной модели т.е. де факто к процедурному программированию, потеряв при этом все преимущества ООП.
Конечно зависимость можно внедрить в агрегат, но тогда он перестанет быть "вещью в себе". А автора ddd парадигмы настаивают, что это неправильно и агрегат не должен зависеть от внешней инфраструктуры. И мне такой подход тоже кажется правильным. В этом случае нет другого варианта, как внедрить зависимость в доменный сервис.
Он и не будет зависеть от инфраструктуры, это инфраструктура будет зависеть от него, для этого и придуман принцип Dependency inversion
Авторы DDD говорят о том что доменный слой не может напрямую зависеть от слоя инфраструктуры. И сервис и агрегат находятся в слое домена. Но вы почему-то допускаете внедрение зависимости в один из компонентов слоя, но не допускаете в другой.
Это пример того как теория может не стыковаться с практикой. Если все нужные справочные данные можно передать в слой домена из вышележащего слоя до запуска алгоритма расчёта, то теория DDD согласуется с практикой. Если же по ходу расчёта в алгоритм надо подтянуть новую порцию внешних данных, то придётся внести внешнюю зависимость или в агрегат или в доменный сервис. Мой выбор - внести зависимость в доменный сервис.
Я не понимаю как инверсия зависимостей между слоями противоречит теории DDD. И чем внедрение зависимости в сервис лучше внедрения зависимости в агрегат. Напротив, в таком подходе я вижу одни недостатки: либо мы скатывается к анемичной модели и процедурному программированию, либо наш алгоритм размазывается по двум классам, что затрудняет его понимание, поддержку и тестирование.
Если алгоритм при расчёте не требует подтягивания новой порции внешних данных, то такой алгоритм можно реализовать внутри агрегата.
С моей токи зрения алгоритм который требует внешних данных также должен быть реализован внутри агрегата. В сервисе нужно реализовывать только тот алгоритм который изменяет состояние нескольких агрегатов одновременно, да и в этом случае можно обойтись без сервиса, хотя это будет значительно сложнее. Единственным ограничением реализации алгоритма внутри агрегата является то, что такой алгоритм может только получать данные из внешних источников, но не изменять их состояние. Изменять агрегат может только свое состояние.
Мне не совсем понятно почему внешнюю зависимость нельзя внедрить в агрегат, но можно внедрить в доменный сервис
Потому что доменный сервис обычно создается на старте приложения и существует в одном экземпляре, а сущность создается в середине выполнения запроса по данным из базы, и количество экземпляров может быть хоть 100 штук. Пробросить зависимость в сервис легко, а в сущность нет.
С логической точки зрения - в бизнес-требованиях при описании логики действия используются термины "заказ" и "товар", а не "этот". Поэтому если мы хотим создать правильную модель описанной логики, в коде метода с реализацией этой логики должны быть переменные "order" и "product", а не "this".
Потому что доменный сервис обычно создается на старте приложения и существует в одном экземпляре, а сущность создается в середине выполнения запроса по данным из базы, и количество экземпляров может быть хоть 100 штук. Пробросить зависимость в сервис легко, а в сущность нет.
При наличии соответствующих инструментов никакой разницы вообще нет.
Поэтому если мы хотим создать правильную модель описанной логики, в коде метода с реализацией этой логики должны быть переменные "order" и "product", а не "this".
Угу, в доменных сервисах у вас по видимому приватных методов с this тоже нет :)
никакой разницы вообще нет
Ну как это нет. И другому программисту сложнее разбираться, что это за аргумент такой или свойство сущности, которого нет в бизнес-требованиях, и сам вызывающий и вызываемый код усложняется, и на производительность 100 лишних ссылок больше влияют, особенно если их инициализировать этими инструментами через рефлексию.
приватных методов с this тоже нет
Есть, но в данном случае использование this не является частью бизнес-логики, его можно игнорировать и обращать внимание только на название переменных и методов. А в вашем подходе this представляет какую-то сущность из бизнес-требований. Другому программисту надо разбираться, какая часть кода относится к логике, а какая к деталям реализации.
Ну как это нет. И другому программисту сложнее разбираться, что это за аргумент такой или свойство сущности, которого нет в бизнес-требованиях,
Но если такой аргумент появится в "доменном сервисе" все сразу станет понятно.
и на производительность 100 лишних ссылок больше влияют, особенно если их инициализировать этими инструментами через рефлексию.
Давайте тогда ORM тоже не использовать, некоторые из них тоже рефлексию используют и ссылки на другие объекты в сущность добавляют
Другому программисту надо разбираться, какая часть кода относится к логике, а какая к деталям реализации.
Ага, не дай бог ещё на функцию которая массив фильтрует, или подстроку находит наткнется в бизнес логике, вконец наверное бедный запутается
Но если такой аргумент появится в "доменном сервисе" все сразу станет понятно.
Так он там не появится, он передается в конструктор сервиса отдельно от сущности, а не в аргументах метода.
Давайте тогда ORM тоже не использовать
От ORM отказаться нельзя без значительных сложностей, а от логики в сущностях можно.
Ага, не дай бог ещё на функцию которая массив фильтрует
Эта функция находится на другом уровне абстракции. Если она будет передаваться в аргументах, или тем более будет среди свойств сущности "createdAt, updatedAt, filterFunction", то да, тоже будет непонятно.
Да, кстати, вы можете использовать такую функцию в сущности только потому что это глобальная зависимость, которую предоставляет рантайм языка. А представьте как будет выглядеть сущность, если все такие зависимости надо будет передавать снаружи, включая объект с методами add/sub вместо "+" и "-". А с сервисом они будут спокойно передаваться в конструктор отдельно от сущностей и их свойств.
Понятно, с вашей точки зрения любой код с любыми зависимостями в "доменных сервисах" априори будет более понятен с точки зрения бизнес процесса, чем такой же код внутри сущности. Из опыта предыдущих дискуссий с вами знаю, что никакие аргументы вас не переубедят, поэтому дальнейшую дискуссию считаю бессмысленной.
Так я же написал критерий понятности - находятся ли технические зависимости вперемешку с бизнес-терминами или отдельно. В сервисах они отдельно. Технические зависимости в конструкторе, бизнес-термины это методы и их аргументы.
знаю, что никакие аргументы вас не переубедят
Я вам говорил, какие аргументы переубедят - напишите код, сравним. Бизнес-требования есть в той статье, где была дискуссия. Я не знаю, зачем надо писать много комментариев, если можно написать код и показать результат.
В качестве примера более подробно рассмотрю алгоритм расчёт теплообменника. Расчёт теплообменника идёт в составе блока воздухоохладителя или воздухонагревателя. Кроме того возможно использование разных типов теплоносителей или холодоносителей. Поэтому есть множество алгоритмов расчёта. Некоторые алгоритмы состоят из более чем 400 формул. Каждый алгоритм распадается на ветви и далее по ходу расчёта ветвь распадается на подветви. При прохождении алгоритма по любой из ветвей используется не менее 200-250 формул. В алгоритме используется множество промежуточных переменных, которые не имеют никакого отношения к агрегату. Поэтому возникает идея полностью разделить между собой объекты агрегата и алгоритма, и объект алгоритма инжектировать в агрегат через интерфейс. Объект алгоритма по ходу расчёта обращается к базе данных для получения справочных данных. Агрегат не зависит от внешней инфраструктуры - внешних источников данных.
Алгоритмы, используемые в доменной логике, можно разбить как минимум на две группы. В первой группе алгоритм встроен в код агрегата. Агрегат создаётся в каком-то первичном состоянии. Затем при обработке внешнего события запускается алгоритм и переводит агрегат в новое состояние. Алгоритм расчёта теплообменника относится к другой группе. Этот алгоритм используется внутри доменного сервиса и до запуска алгоритма агрегат вообще не существует. При запуске алгоритма в него передаётся набор начальных данных для расчёта теплообменника. В общем случае алгоритм рассчитывает целый набор агрегатов-теплообменников, который отображается на визуальной форме. Пользователь из набора выбирает один агрегат и с ним идёт дальнейшая работа в соответствии с используемой бизнес-логикой.
Domain-Driven Design: чистая архитектура снизу доверху