Когда мидл-разработчик дорастает до сеньора, его, обычно мучает вопрос: «как правильно писать приложение?» Понятно, что когда он был джуном, ему давали совсем атомарные задачи и он развлекался покрытием тестов или написанием контроллеров. Переход в мидлы знаменуется назначением разработчику более абстрактных задач вроде реализации сервисов, репозиторной части или интеграции с внешними сервисами посредством клиентов. Но в какой‑то момент мидл начинает задавать самому себе вопросы: «как найти единственно правильный способ написать приложение с нуля?»
Если вы мидл и вас стали мучать такие вопросы — поздравляю, вы на верном пути. Ведь профессиональный рост не происходит с переводом на должность. Новый сеньор должен родиться, и это как раз муки такого рождения.
Да, мы уже знаем самые популярные практики: KISS, DRY, YAGNI, SOLID, что там ещё... Мы умеем их применять. Но нас не покидает чувство, что все эти практики объединяет общая научная основа. Знаете, это как с Менделеевым, который на основе закономерностей практически по наитию составил периодическую систему, а потом открыли электроны и всё встало на свои места.
У меня для вас хорошие новости: научная основа есть. Это предметно-ориентированное проектирование.
Но есть и плохая новость: тема настолько новая и непростая в изучении, что какая-никакая популярность к ней пришла только лет 5 назад, и до сих пор совсем немного разработчиков достаточно хорошо в ней разбирается.
Но есть ещё одна хорошая новость: в статье я постараюсь дать максимально понятный ответ, что же такое предметно-ориентированное проектирование.
И начнём мы, конечно, с антипримера: плохой архитектуры.
Плохая архитектура
Итак, что же такое плохая архитектура? Чем она плоха?
Плохая архитектура имеет вполне конкретные критерии. Давайте разберём основные и, что самое главное, определим, чем же они так плохи для разработки.

И первый критерий — это хрупкость.
Хрупкость — это состояние системы, при котором один компонент отвечает за множество реализаций.
Изменив такой компонент, вы рискуете изменить логику приложения ещё в нескольких местах.
Возьмём пример из книги Роберта Мартина "Чистая архитектура".
В нашем приложении есть функция, которая умеет считать сверхурочное время сотрудника. Она настолько полезна, что ею пользуется не только отдел кадров (для учёта переработки сотрудников во внутренних отчётах), но и бухгалтерия (чтобы начислять зарплату).

И отдел кадров, и бухгалтерия вызывают одну и ту же функцию.
Но вот вышел новый нормативный акт, который обязывает отдел кадров иначе учитывать переработки сотрудников: по повышенному коэффициенту. Разработчик исправляет коэффициент и отправляет изменения в прод. Отдел кадров удовлетворён.
Наступает 5 число, отдел кадров засыпает и просыпается бухгалтерия. Для бухгалтерии нормативные акты не поменялись, но она теперь рассчитывает переработки по повышенному коэффициенту. И работники получают больше денег за переработки.
Хорошо всем, кроме бухгалтерии. Она требует вернуть всё как было, но как это сделать, когда в единственной функции учтены новые требования отдела кадров?
Это и есть хрупкость. Мы сделали одно движение, а сломалось в нескольких местах.
Как быть?
Очевидным решением будет применение Принципа Единственной Ответственности. Который прямо обязывает нас следить за тем, чтобы один компонент отвечал за что-то своё. Мы напишем две функции — для отдела кадров и бухгалтерии, — каждая из которых будет жить своей жизнью и отвечать перед кем-то одним.

Хорошо. Бывают ситуации, когда изменение одного компонента ломает систему во многих местах. Но бывают ли ситуации, когда мы вообще не можем ничего изменить?
Да, и это ещё один критерий плохой архитектуры. Жёсткость.
Жёсткость – это свойство системы, при котором любое изменение одного компонента неизбежно затрагивает другие.
На практике это означает, что жёсткие места очень сложно изменить. В случае хрупкости, любое изменение неконтролируемо ломает систему. Жёсткую же систему очень сложно изменить в принципе.
Возьмём пример из жизни.
Уважаемые коллеги написали приложение, в котором есть контроллер, сервис и репозиторий. Поскольку контроллеру нужно было DTO, то уважаемые коллеги решили брать его прямо из репозитория, дообогащать его разными полями из других приложений в сервисе и возвращать в контроллер.

Всё приложение было жёстко завязано на одно-единственное DTO. Ключевое слово: жёстко. Это DTO требовалось по контракту, и про него знал даже репозиторий. Других вариантов получить что-то кроме этого DTO, как мы понимаем, не было.
А теперь представим ситуацию, когда в другом методе контроллера нужно получить другое DTO. Представили? А я вот нет. Задача, которая при первой оценке выглядела на полдня, приобрела немыслимые размеры. Ведь чтобы вернуть другое DTO, нужно было, по логике разработчиков, познакомить с новым DTO все слои аж до репозитория. При этом, данные нужно взять из той же таблицы и обогатить новое DTO теми же данными, что и первое. Но сделать это в текущей архитектуре решительно невозможно. Потому что все сервисы жёстко насажены на контракт.
И не забывайте, что DTO — это компонент контракта с фронтендом. Что будет, если потребуется его изменить? Нам опять же, придётся переделывать всё вплоть до репозитория.

Как быть в этой ситуации? Противопоставлением жёсткости является гибкость.
Гибкость — это свойство системы, позволяющее изменять её с минимальными последствиями.
Что это значит в нашем случае? Какой должна быть система, чтобы мы могли создать новый endpoint с новым DTO за половину рабочего дня?
Очевидно, что если нам требуется изменить только контракт, то изменения должны коснуться только слоя контроллера. Поскольку именно он отвечает за контракт.
Как это сделать?
Что, если в каждом слое будет своя структура данных, которая будет подчиняться потребностям этого слоя и отвечать за передачу данных только в его пределах?
Сервис будет работать с бизнес-сущностью, репозиторий будет работать с репозиторной сущностью, а с DTO будет работать только контроллер? В таком случае, для того, чтобы сделать новый контроллер, нам потребуется просто написать его, написать новое DTO и преобразовать данные, полученные из сервиса, в это DTO.

Да, я уже слышу возражения: но ведь тогда нам придётся писать больше кода. Нам нужно будет спроектировать бизнес-сущности, репозиторные сущности, DTO контроллеров. Уйма времени. И я вам возражу в ответ: но ведь тогда стоимость любой доработки будет одинаковой. Вы точно сможете оценить длительность выполнения задачи на планировании, не боясь провалиться в кроличью нору. И стоимость эта, кстати, будет невысокой. Поскольку дописать немного нового намного дешевле, чем переписать уже имеющийся кусок.
И всё-таки, ценой невероятного героизма и кровавого рефакторинга, можно избавиться от жёсткости и сделать приложение более гибким. Но бывают ситуации, когда даже рефакторинг нам не по силам.
Эти ситуации возникают, когда приложение имеет признаки третьего критерия плохой архитектуры — неподвижности.
Неподвижность — это свойство приложения, которое написано с учётом такого количества специфичных особенностей, что переиспользовать эти компоненты практически невозможно.
Представим ситуацию.
Например, вы любите Hibernate.
И вы написали приложение, в котором использовали Spring Data JPA вместе с Hibernate.
Hibernate, как мы знаем, позволяет привязать таблицы к сущностям прямо на их стороне. Вы взяли сущность и обогатили её аннотациями, которые чётко дают понять, что она работает с реляционной базой данных. Какие это могут быть аннотации? Table
, Entity
, Column
, GeneratedValue
, OneToMany
и прочие. Реализация хранения данных в реляционной базе приколочена намертво.

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

Решение, конечно, есть. И о нём, а точнее, о концепции проектирования, объединяющей все эти хорошие практики и встраивающей их в общую логику, мы и поговорим сегодня.
Это предметно-ориентированное проектирование, или Domain-Driven Design.
Domain-Driven Design
Предметно-ориентированное проектирование является более абстрактным воплощением объектно-ориентированного подхода, предоставляя более основательную научную базу.

Что я имею в виду?
Объектно-ориентированное программирование распространяется на применение языка. предметно-ориентированное проектирование позволяет распространить объектный подход на все уровни разработки — от архитектуры приложений до межсервисной архитектуры.
Предметно-ориентированное проектирование включает в себя три понятия:
Предметная область;
Ограниченный контекст;
Единый язык.
Разберём каждое из них.
Предметная область
Предметная область — это совокупность тесно связанных понятий, описывающих свойства и поведение в пределах интересов бизнеса.
Что это значит на реальном примере?
Мы пишем интернет-магазин. Какие понятия описывают его свойства и поведение?
Продавец, Покупатель, Товар, Корзина, Баланс, Цена. Совокупность этих понятий и будет являться предметной областью интернет-магазина.
Предположим, для интернет-магазина нужна служба доставки. Её предметная область будет совершенно другой.
Посылка, Автомобиль, Маршрут, Адрес.
Хотим реализовать собственную систему оплаты? Пожалуйста: Лицевой Счёт, Баланс, Сертификат, Чек, Покупатель.

Каждое из этих приложений обладает собственной предметной областью.
Хорошо, мы захотели написать такое приложение. Как нам организовать предметные области разных компонентов, чтобы они не мешали друг другу?
Здесь нам поможет Ограниченный Контекст.
Ограниченный контекст
Ограниченный контекст — это понятийные границы бизнеса, в которые заключена предметная область.
Итак, мы определили предметную область для каждого из компонентов нашего приложения. Но если мы захотим написать его, то разные предметные области будут смешаны, что сделает наше приложение неподвижным.
Ведь мы не сможем использовать эти компоненты отдельно друг от друга, а хотелось бы иметь такую возможность.
Например, мы захотим открыть пиццерию. Тогда хорошей идеей будет переиспользовать службу доставки. И систему оплаты тоже.
Чтобы этого избежать, необходимо каждую из предметных областей ограничить своим контекстом, жёстко инкапсулируя предметные области в своих границах.

В практическом плане предметные области могут быть разделены по пакетам, модулям или, что наиболее предпочтительно, по отдельным сервисам. При этом количество связей между ограниченными контекстами должно стремиться к нулю.
Итак, мы разделили предметные области при помощи ограниченного контекста. Осталась маленькая деталь: договориться внутри команды (а под командой подразумеваются все, включая заказчика) о терминах предметной области. Такой коллективный язык называется Единым Языком.
Единый язык
Единый язык — это общий язык терминов и понятий, описывающих предметную область в рамках ограниченного контекста.
Основная часть Единого Языка приходит в проект от бизнеса. Если вам в процессе разработки приходится сталкиваться с огромным количеством понятных только вашей команде сокращений и аббревиатур, то поздравляю — это и есть Единый Язык.
Единый Язык — это набор терминов и понятий, которые одинаково понимаются всеми участниками процесса разработки. Это позволяет избежать недопонимания и ошибок при обсуждении требований и реализации функциональности.
Итак, три основных понятия, на которых строится предметно-ориентированное проектирование:
Предметная область;
Ограниченный контекст;
Единый язык.
Для закрепления материала нам осталось научиться применять предметно-ориентированное проектирование на практике. Затем мы применим концепцию DDD в рамках межсервисной и внутрисервисной архитектуры.
Но прежде давайте разберём такое важное явление, как интерфейсы.
Интерфейсы
Что такое интерфейс?
Интерфейс — это место подключения двух независимых систем.
К примеру, у нас есть сервис, и мы хотим иметь возможность подключать его к другим сервисам. Поскольку мы используем предметно-ориентированное проектирование, предметная область нашего сервиса ограничена своим контекстом. Нельзя просто так взять и влезть в предметную область нашего сервиса.
Именно поэтому в контексте предметно-ориентированного проектирования невероятно важным компонентом является интерфейс. Поскольку мы максимально инкапсулировали предметную область, нам необходимо создать API, который позволит нашему приложению взаимодействовать со внешним миром.
И это будет интерфейс.
Интерфейс должен иметь контракт, позволяющий другим сервисам взаимодействовать с нашим.

Интерфейсы есть везде, где есть жёстко инкапсулированная система.
Классы взаимодействуют друг с другом через интерфейсы. Сервисы взаимодействуют друг с другом через API, которые также являются интерфейсами. Устройства взаимодействуют друг с другом через интерфейсы, коими являются, например, USB-порт или Wi-Fi-протокол. Даже человек взаимодействует с системой через пользовательский интерфейс.
И в нашем примере тоже будут интерфейсы.
Матрёшка ограниченных контекстов
Теперь, когда мы разобрались с интерфейсами, разберём иерархию мультисервисного приложения с точки зрения предметно-ориентированного проектирования.
У нас уже есть некий сервис, коим является микросервисное приложение «Интернет-магазин».
У «Интернет-магазина» есть своя предметная область, свой ограниченный контекст и свой единый язык.
API такого интернет-магазина будет включать в себя такие же высокоабстрактные понятия: добавление в корзину, пополнение счёта, оплата, доставка, отзыв. Мы, как потребители, взаимодействуем с интернет-магазином через интерфейс — пользовательский интерфейс.

Если мы опустимся на уровень ниже, то увидим всё то же самое: некоторое количество сервисов, имеющих уже свои предметную область, ограниченный контекст и единый язык. И интерфейс, фиксирующий контракт каждого из них на функциональность.
Ещё уровнем ниже мы найдём более низкоуровневые компоненты, имеющие всё то же самое.
И так вплоть до бизнес-сущностей.
Стратегическое проектирование. Межсервисное взаимодействие на примере
Итак, у нас есть несколько микросервисов, которые являются частью одной системы.
Нам необходимо спроектировать архитектуру их взаимодействия таким образом, чтобы она нас устроила.
Для того, чтобы считаться хорошей, архитектура должна соответствовать как минимум трём критериям:
Функциональная целостность.
Принцип «Вход — Процесс — Выход».
Слабые связи между компонентами.
Как наши модули будут взаимодействовать между собой? Наиболее очевидным способом будет перекрёстный вызов микросервисов по мере надобности. Например, если Витрине будут нужны детали Доставки, она может обратиться напрямую в Доставку. А сервис Биллинга может обратиться к Витрине, чтобы получить общую стоимость товаров и выставить счёт.

Тем не менее, перекрёстные вызовы между микросервисами нарушают сразу несколько требований к хорошей архитектуре.
Во-первых, наличие множества связей между компонентами делает компоненты логически зависимыми друг от друга. Не получив данные, такой сервис не сможет завершить собственную логику. Повышается сцепка между сервисами. Их логика становится созависимой, и такую систему намного сложнее поддерживать и развивать.
Система становится жёсткой.
Ситуация усугублятся, когда разные компоненты пишут разные команды. Выход? Нужно инкапсулировать сервисы друг от друга и исключить связи между ними.
Вторая проблема: где хранить совместные сценарии?
Предположим, Биллингу нужно получить от Витрины сумму покупок, а от Доставки — стоимость доставки. Или, например, Доставке нужно получить список товаров в рамках своего сценария. При всей кажущейся целостности системы сценарии будут размазаны по различным компонентам, что затруднит понимание системы и её поддержку. Такие сценарии явно превосходят предметную область каждого отдельного сервиса и не вмещаются в ограниченный контекст каждого из них.

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

Приложение становится хрупким.
Как насчёт Kafka?
А что, хорошая идея. Более того, Эрик Эванс прямо рекомендует асинхронный обмен как наиболее подходящий при предметно-ориентированном проектировании. Какие проблемы нам удастся решить при помощи брокера сообщений?
Совершенно точно удастся решить проблему инкапсуляции сервисов. Теперь они не будут делать перекрёстные запросы, а просто станут откидывать и слушать события. Наше приложение стало менее жёстким, и это уже хорошо. Но что в остальном?

А в остальном у нас всё так же. По-прежнему нет места для общих сценариев (брокер же не может хранить их), и нам нужно поддерживать точки входа для внешних потребителей. Приложение осталось неподвижным и хрупким.
Избавимся от сценариев и внешнего API?
Если невозможно победить хаос, его нужно возглавить. Что, если нам попросту избавиться от сценариев и внешнего API, раз они нам так мешают? Не насовсем, конечно, а вынеся их за пределы микросервисов, на уровень выше?
Мы создадим сервис, который будет знать все микросервисы и зависеть от них. Который будет отвечать за сценарии, внешний API и коммуникации между микросервисами. И Kafka будет не нужна.
Диспетчер?
Выделим несколько архитектурных паттернов, которые помогут нам создать такой сервис:
Фасад — для организации внешнего API.
Оркестратор — для хранения сценариев и оркестрирования работы микросервисов.
Посредник — для обеспечения взаимосвязи между микросервисами, сохраняя при этом инкапсуляцию между ними.
Пробежимся по каждому из паттернов.
Фасад. Скрывает множество API разных микросервисов, предоставляя общий единый API. Отныне многочисленные клиенты будут знать только его и взаимодействовать только с ним. Соответственно, изменения API внутренних сервисов будут слабо влиять на внешний API, особенно в той части, которая касается внутренних сценариев. Иными словами, минус хрупкость.

Из недостатков: многие endpoint-ы будут проксировать запросы в нижестоящие сервисы.
Оркестратор. Позволяет поднять сценарии на уровень выше. Нижестоящие микросервисы ничего не знают про сценарии, их работа отныне сведена к атомарным задачам. Минус неподвижность.

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

Фуф, с межсервисной архитектурой мы разобрались. Но как быть с архитектурой внутрисервисной? Как обеспечить следование предметно-ориентированному проектированию при написании каждого микросервиса?
Спустимся на уровень ниже.
Тактическое проектирование. Внутрисервисная архитектура
Мы разобрались, как применять принципы предметно-ориентированного проектирования на межсервисном уровне. Давайте разберёмся, как применять эти принципы на уровне внутрисервисном.
Поможет нам в этом луковичная архитектура.
Луковичная архитектура
Луковичная архитектура очень наглядно описывает то, как предметная область ограничивается своим контекстом. Так же, как и в предметно-ориентированном проектировании, предметная область является центральным элементом структуры приложения. Только называется по-другому: бизнес-сущности.
Бизнес-сущности
Бизнес-сущности ничего не умеют делать и ничего не знают. Они полностью беспомощны. Они просто существуют, и всё. Их единственная задача — наполнять своим существованием предметную область приложения.

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

Агрегат — это полностью самодостаточная бизнес-сущность, имеющая все свойства, чтобы не от кого не зависеть.
Например, в нашем микросервисе «Витрина» существует Агрегат «Товар». Этот Агрегат будет содержать в себе всё, что касается товара, например:
свойства Товара;
корзины, в которых добавлен Товар;
историю изменения стоимости Товара, и так далее.
Агрегат включает в себя компоненты трёх типов:
корень Агрегата;
другие бизнес-сущности;
объекты-значения.
Корень Агрегата — это ключевая сущность Агрегата. Она владеет всеми остальными его элементами. Имя Корня Агрегата является концептуальным именем Агрегата. Для нашего Агрегата «Товар» Корнем будет бизнес-сущность Good.
Объекты-значения — это объекты в пределах Агрегата, которые не являются самостоятельными сущностями, а содержат специфичные для Агрегата данные, не применимые и не имеющие смысла за его пределами. У Объекта-значения даже нет собственного идентификатора в системе, он инициализируется вместе со своим Агрегатом.

Для нашего Агрегата «Товар» таким Объектом-значением может являться История изменения стоимости. Мы не можем применить стоимость товара в отрыве от Товара где-то ещё, поэтому эти данные имеют смысл только в привязке к Товару и могут быть внутренним классом.
Другие сущности тоже могут входить в Агрегат в качестве его частей. При этом они могут использоваться отдельно от него Агрегата и входить в состав других Агрегатов. Такие сущности имеют собственную идентичность и хранятся отдельно.
Казалось бы, что может быть проще бизнес-сущностей? Ан нет, выясняется, что и они чётко структурированы в пределах предметной области. Мы храним бизнес-сущности в пакете domain на верхнем уровне компоновки.
Идём дальше.
Сервисы
Как мы выяснили ранее, бизнес-сущности сами по себе ничего не умеют. Умения бизнес-сущностей сконцентрированы в сервисах.
Сервис — это класс, который реализует бизнес-логику.
Сервис состоит из двух обязательных компонентов:
интерфейса, предоставляющего контракт и декларирующего бизнес-логику;
N-ного количества реализаций

Работа сервисов основана на правилах.
Правило первое: сервис работает только со своей бизнес-сущностью и ничем другим. Или, если точнее, с Агрегатом. Сервисы работают с другими Агрегатами только через их сервисы. Это важно. Сервис не может работать с другими Агрегатами напрямую, только со своим.
Могут ли сервисы работать с несколькими сущностями сразу? Могут. Но это не сервисы в привычном понимании. И эта тема настолько обширна, что тянет на отдельную статью. Разберём это в другой раз.
Правило второе: архитектура зависимостей сервисов должна быть подчинена жёсткой иерархии и повторять иерархию Агрегатов. Сервисы не должны быть циклично зависимы. Если сервис А зависит от сервиса Б, то Б не может зависеть от А.
И, наконец, правило третье. Сервисы взаимодействуют друг с другом только через интерфейсы.
Точки взаимодействия предметных областей
Хорошо. Мы разобрали концепцию предметно-ориентированного проектирования. Изучили предметные области, определили роль ограниченного контекста, познакомились с единым языком. У каждого нашего сервиса присутствуют все атрибуты DDD. Всё круто, мы красавчики.
Но есть один момент. Эти уникальные предметные области, жёстко инкапсулированные при помощи ограниченных контекстов, должны как-то взаимодействовать. И не где-то в абстракции, а в конкретном месте в коде вашего сервиса.
Где? Предположим, у нас есть бэкенд, фронтенд и база данных. Предметная область бекенда состоит из бизнесовых понятий. Предметная область фронтенда состоит из элементов UI. Предметная область базы данных состоит из колонок и таблиц, скриптов и хранимых процедур.
Бэкенд ничего не знает про UI фронтэнда и таблицы базы данных. Фронтенд ничего не знает про бизнесовые понятия и колонки с хранимыми процедурами. База данных ничего не знает про бизнес-сущности и UI. И всё это пересекается где-то в бэкенде.
Но и в бэкенде должно быть место, где эти предметные области соприкасаются. И это место: Data Access Object.
Data Access Object
Data Access Object является третьим слоем «луковичной архитектуры». В отличие от сервисного слоя, DAO не подчинён предметной области приложения. Он подчинён предметной области внешнего интерфейса. И именно DAO является тем местом, где предметные области взаимодействуют между собой.
Data Access Object — архитектурный слой, взаимодействующий с внешним интерфейсом и подчинённый его предметной области. В слое DAO происходит стратегическое связывание предметных областей.
Как происходит связывание предметных областей на конкретном примере? У нашего приложения есть предметная область — бизнес-сущности (Агрегаты). Такая предметная область есть и у стороннего сервиса — это Data Transfer Object (DTO). Как мы уже выяснили, преобразование бизнес-сущности в DTO и обратно происходит в слое DAO. Такое преобразование происходит в специальном месте — маперах.

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

Таким образом, местом взаимодействия двух предметных областей является слой DAO. Местом преобразования предметных областей являются маперы в пределах своего компонента DAO.
Подведём итог
Что даёт понимание предметно-ориентированного проектирования? Зачем нам это? Ведь можно же просто писать код и не париться.
Усложнение информационных технологий неизбежно.
Повышение уровня абстракции информационных систем неизбежно.
Развитие теории программирования неизбежно.
Когда я учился в школе и писал программы не Бейсике на советском компьютере УК-НЦ, для этого достаточно было выучить команды языка. Большинство программ были утилитами, вроде калькулятора, а компьютер на столе офисного работника был диковинным показателем статуса.

Но ситуация меняется. И слишком быстро.
Информационные системы огромны и абстрактны. Они объединяются в виртуальные сети, дополняя друг друга. Они развивают сами себя. Они захватили наш мир.
В последние полвека в сфере информационных технологий произошёл Большой Взрыв.
Чтобы взрыв оставался управляемым, нам, разработчикам, нужно иметь основательную базу теории программирования. Теория программирования должна развиваться так же быстро, как системы, которые мы строим. Иначе, в один момент всё рухнет, как плохо спроектированный мост. И результаты будут катастрофическими.

Предметно-ориентированное проектирование — это один из кирпичиков того теоретического фундамента, на котором держится квалификация лучших разработчиков. И я надеюсь, что эта статья пролила свет на эту теорию.
Комментарии (58)
GcVit
29.11.2024 08:22Спасибо. Очень качественно изложено. Похоже DDD, не только методология качественного проектирования, но и хорошо работает для построения любых сложных рассуждений.
breninsul
29.11.2024 08:22А будет какое-то обоснование "сервисы должны взаимодействовать только через интерфейсы"? Что-то кроме "так правильно"? Бесполезный Heder Interface просто повторяющий паблик-методы это же так чистокодно!
Serge1001
29.11.2024 08:22Ну завязка на реализацию не есть хорошо. Вдруг потом захочется подменить и придётся всё переписывать.
Хотя конечно понимаю о чём вы, чаще всего это будет излишней преждевременной оптимизацией
michael_v89
29.11.2024 08:22Вдруг потом захочется подменить и придётся всё переписывать.
Что "всё"? Заменить
import A.B.SomeClass
наimport A.C.SomeClass
в нескольких файлах?
onets
29.11.2024 08:22Наконец-то, а продолжение будет? Покажите код. Есть что нибудь сложнее хеллоу ворлд?
Особенно интересуют всякие приколюхи типа подгрузки данных из хранилища в процессе работы или текучие абстракции, когда доменная модель через инверсию зависимости начинает знать о репозитории…
mike_shapovalov
29.11.2024 08:22Бизнес-сущности ничего не умеют делать и ничего не знают. Они полностью беспомощны. Они просто существуют, и всё. Их единственная задача — наполнять своим существованием предметную область приложения.
Как мы выяснили ранее, бизнес-сущности сами по себе ничего не умеют. Умения бизнес-сущностей сконцентрированы в сервисах.
Вы описали антипаттерн который противоречит основным принципам ООП и тактическим паттернам 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.
Полностью можно ознакомиться тут:
maratxat
29.11.2024 08:22тоже удивился, что бизнес модель оставили без бизнес логики
для чего тогда автор писал про агрегаты - непонятно...
michael_v89
29.11.2024 08:22Этот Агрегат будет содержать в себе всё, что касается товара, например:
свойства Товара;
корзины, в которых добавлен Товар;
историю изменения стоимости Товара, и так далее.Да вы что, Товар и Корзина это совершенно разные агрегаты. Товар может существовать без корзины, если на сайте нет функционала покупок, а корзина может работать только с 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 }
. Или например дробное значение можно хранить в виде значения типа float1.234
, в виде строкового значения"1.234"
, а можно в виде объекта{"integer": 1, "fractional": 234}
. Вот этот объект и называется value object.Сервис — это класс, который реализует бизнес-логику.
Ну как бы по DDD бизнес-логика должна быть в сущностях, а не в сервисах.
chaetal
29.11.2024 08:22Так же, как и в предметно-ориентированном проектировании, предметная область является центральным элементом структуры приложения. Только называется по-другому: бизнес-сущности.
Бизнес-сущности
Бизнес-сущности ничего не умеют делать и ничего не знают. Они полностью беспомощны. Они просто существуют, и всё. Их единственная задача — наполнять своим существованием предметную область приложения.
И именно они являются той самой предметной областью.
Воняет Data Class-ом, аж не продохнуть!
Объекты, которые ничего не умеют делать, — это не объекты. "Или крестик снимите, или трусы наденьте" — либо делайте нормальные живые объекты, либо удаляйте отсылки к ООП, когда про процедурное программирование рассказываете.
Avatap
29.11.2024 08:22Писец, хочу как бы напомнить что плодить сущности без разбора тоже не есть хорошо. Т.к. современные компы плохо в параллельность
AlexViolin
29.11.2024 08:22Прокомментирую раздел статьи, который касается принципов предметно-ориентированного проектирования на внутрисервисном уровне.
Насколько представляю себе из литературы по ddd агрегат как раз должен в себе содержать функционал, связанный с работой его внутренней бизнес-логики. И выделять отдельно сервис для обработки данных отдельного агрегата надо только исходя из каких-то специфических требований. А в общем случае доменные сервисы нужны, если требуется одновременная обработка данных для двух и более агрегатов. Но есть другая проблема про которую не встречал упоминания в литературе, но она не так уже и редка и в моих проектах встречалась многократно. Например внутри агрегата идёт расчёт сложного алгоритма - типовой пример расчёт теплообменника. В зависимости от того по какой ветке пошёл расчёт, алгоритму требуются специфические (именно для этой ветки алгоритма) справочные данные из базы данных. И приходится агрегат подключать к слою persistence layer для получения данных из бд. Агрегат получает зависимость от другого слоя приложения. Но это противоречит определению агрегата доменной модели, как автономной сущности не зависящей от внешнего окружения домена. На текущий момент времени мне не удалось найти описания или хотя бы обсуждения решения подобной проблемы. Под проблемой понимаю внесение в агрегат зависимости от внешнего окружения домена.mike_shapovalov
29.11.2024 08:22Можно эту зависимость инвертировать, вот тут описан подход который мы используем https://habr.com/ru/articles/799019/
mvv-rus
29.11.2024 08:22В зависимости от того по какой ветке пошёл расчёт, алгоритму требуются специфические (именно для этой ветки алгоритма) справочные данные из базы данных. И приходится агрегат подключать к слою persistence layer для получения данных из бд. Агрегат получает зависимость от другого слоя приложения. Но это противоречит определению агрегата доменной модели, как автономной сущности не зависящей от внешнего окружения домена.
Дык, не надо натягивать
совуподход к проектирования (точнее даже, свое понимание его) наглобусзадачу, которую для которой он подходит плохо. Следуйте совету великого русского писателя Козьмы Пруткова "Зри в корень!".Если зрить в корень, выделить главное, основное, то агрегат - это набор сущностей, которые должны меняються согласованно, чтобы сохранялись инварианты предметной области, и этим его выделение полезно. Все остальное - выделение корня, изоляция с разрешением доступа через корень - это уже методы гарантировать такой порядок изменения. Вы задайте себе вопрос - должны ли справочники меняться вместе с сущностями агрегата? И вообще - должны ли они меняться, кроме как пополняться новым содержимым и делать неактуальными часть старого (которое тем не менее остается доступным для ссылки из уже разработанных и ссылающихся на него проектов)? Нет? Тогда справочники меняются независимо, если вообще меняются, и нечего справочникам в агрегате делать. Куда и как тянуть связь к ним - решайте по задаче сами, не оглядываясь ни на какой подход проектиктирования: он неизбежно ограничен, ибо жизнь всегда богаче любой теории.
Впрочем, мне смутно припоминается, что в DDD есть способы описания и для такой задачи: на ум приходит что-нибудь типа сервисов предметной области или вообще интерфейсов к другому ограниченному контексту. Но я не теоретик, а потому точно не скажу.
michael_v89
29.11.2024 08:22На текущий момент времени мне не удалось найти описания или хотя бы обсуждения решения подобной проблемы.
Решение есть, вполне себе простое - не использовать логику в сущностях, а использовать логику в сервисах. Обсуждения этого встречаются довольно часто. Проблемы с логикой в сущностях начинаются как раз из-за внедрения зависимостей.
В рамках DDD нормального решения нет, есть разные обходные пути, один из них это передавать все зависимости в аргументах метода. При этом используется такой самообман, что типа интерфейс зависимости принадлежит домену, а реализация техническому слою, поэтому передавать интерфейс в сущность это нормально. Хотя их использование в коде метода выглядит одинаково.
mike_shapovalov
29.11.2024 08:22При этом используется такой самообман, что типа интерфейс зависимости принадлежит домену, а реализация техническому слою,
У этого "самообмана" даже официальное название есть Dependency inversion principle :)
https://en.m.wikipedia.org/wiki/Dependency_inversion_principle
michael_v89
29.11.2024 08:22Да неважно, есть ли у него название) Слой определяется функциональностью, а функциональность выражается в названии. У реализации и интерфейса одинаковые названия методов, поэтому интерфейс находится в том же слое, что реализация. Если вы передаете в метод сущности аргументы Balance или Product, и вместе с ними SomeRepositoryInterface, то получается смешивание уровней абстракции, потому что понятия Balance и Product в бизнес-требованиях есть, а понятия SomeRepository там нет.
mike_shapovalov
29.11.2024 08:22Вы не поверите, но для решения этой проблемы тоже придумали "самообман" https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)#Pure_fabrication
И между прочим ваши доменные сервисы и есть одной из реализаций этого паттерна :)
michael_v89
29.11.2024 08:22Если под решением вы подразумеваете "не передавать зависимости в сущность", то я так сразу и сказал.
mike_shapovalov
29.11.2024 08:22Нет, я имел ввиду что в коде доменной области у вас никак не получится избавится от объектов, которые не имеют непосредственного отражения в предметной области, и ваш "доменный сервис" ничуть не лучше "репозитория" либо любой другой "Pure Fabrication" которая облегчает имплементацию доменной логики.
michael_v89
29.11.2024 08:22А вот как раз лучше. Когда мы передаем репозиторий в сущность, мы смешиваем бизнес-логику с деталями реализации. А с сервисом мы передаем все технические зависимости в конструктор, и в методах приходят только аргументы, которые представляют что-то из бизнес-логики. В сущности тоже остаются только свойства, которые мы выделили при анализе предметной области. Код метода в сервисе содержит детали реализации, это точка соединения разных уровней абстракции, там нормально обращаться к техническим зависимостям, потому что из этого как раз реализация и состоит. Технические зависимости лежат в свойствах сервиса, и бизнес-терминов там нет. Всё красиво сгруппировано.
Сам сервис с логикой, как ни странно, тоже имеет отражение в предметной области. Это инструкция. В бизнес-требованиях есть инструкция как что-то делать - как создать заказ, как рассчитать теплообменник. Метод сервиса это модель этой инструкции, код метода должен содержать шаги из инструкции. Если поменялись требования, находим соответствующий метод и меняем нужные шаги. Не надо переписывать половину приложения потому что после новых требований оказалось, что мы поместили логику не в ту сущность.
mike_shapovalov
29.11.2024 08:22Мне кажется что мы заходим на очередной круг, когда я вам пытаюсь объяснить преимущество ООП перед процедурным программированием. Мне не удалось это сделать в дискуссиях под другими статьями, видимо не удастся и здесь, Если процедурный подход работает для систем которые вы строите, используйте его дальше. Возможно, когда/если появятся проблемы, вы еще раз посмотрите в сторону ООП и поймете преимущества подхода, когда объект сам контролирует свои инварианты.
michael_v89
29.11.2024 08:22Так вот в том и дело, что я вам пытаюсь объяснить, что это вы неправильно представляете, что такое ООП :)
Да, методы, изменяющие данные, должны находиться в одном классе с данными, но эти методы должны работать исключительно с полями класса и ни с чем другим. Если вам нужны какие-то внешние зависимости, значит этот метод не должен находится в классе. Методы могут принимать данные в аргументах, но вам нужен код, который будет подготавливать эти данные, и он должен быть вне сущности.
Условно говоря, вы пытаетесь запихнуть в драйвер файловой системы функции записи видео, потому что запись видео меняет данные файла и содержит какие-то ограничения на них (инварианты). Но это неправильно, система записи видео должна снаружи управлять системой записи данных в файл, в этом нет никакой проблемы.
Процедурное программирование как раз отличается от программирования с объектами. Потому что у объектов есть конструктор. Именно поэтому это ООП.
Возьмем такой пример.
Пример
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. То есть даже в вашем коде даже сущностей не будет. Где вы будете писать бизнес-логику?
mike_shapovalov
29.11.2024 08:22Я вам уже показывал примеры нормальных доменных объектов с поведением в дискуссиях под другими статьями. Повторять одни и те же аргументы еще раз я не вижу смысла, вы их не слышите. Если вы считаете что наличие класса с конструктором делает ваш код объектно-ориентированным, то у нас с вами представление о ООП очень сильно отличаются, и мы все равно друг друга не поймем.
michael_v89
29.11.2024 08:22Объектно-ориентированным код делает наличие объектов. Всё. Именно поэтому оно так называется.
Представления у нас в целом одинаковые, просто вы неправильно представляете критерии, по которым определяется принадлежность методов объекту - что является деталями реализации, что бизнес-логикой, к какому объекту относится инвариант.Попробую еще раз объяснить. Вот вы говорите про инварианты. Инвариант самой сущности может относится только к деталям реализации ее свойств. Вот есть у вас сущность, где вы храните все свойства в одном поле
$data
, в методах вы можете например контролировать, что в нем не появится новых свойств. Но сущность не может контролировать бизнес-требования, потому что она их не задает, они задаются бизнесом и существуют отдельно от сущности, сущности появляются при их анализе. Бизнес-требования первичны, а сущности вторичны.
Кстати, подумайте, как будет выглядеть бизнес-логика с полем$data
. Тут наглядно видно, как смешиваются разные уровни абстракции.Инварианты, связанные с бизнес-требованиями, не принадлежат сущности, хотя бы потому что могут быть бизнес-требования, в которых инвариант затрагивает несколько сущностей. "У пользователя не может быть более 3 статей на модерации". Это надо проверять при отправке статьи на модерацию, то есть в вашем подходе этот код должен быть в сущности Article. Но это не инвариант одной этой Article. При этом для соблюдения логического инварианта тут еще нужны вполне технические мьютексы на процесс "Отправка на модерацию пользователем N", которые должны освобождаться только после сохранения в базу. То есть либо вам надо перенести сохранение в базу и работу с мьютексами в сущность, либо часть логики соблюдения инварианта будет вне сущности.
и мы все равно друг друга не поймем.
Конечно, если игнорировать неудобные вопросы, то и понять будет нельзя. Еще раз предлагаю вам подумать над вопросом, где вы будете соблюдать инварианты в случае бизнес-требований к вызовам GraphQL API.
mike_shapovalov
29.11.2024 08:22Попробую еще раз объяснить.
Не трудитесь. Все это мы уже с вами не раз обсуждали в других ветках, с примерами кода. Но у вас есть свое, очень специфическое понимание ООП которое мне, к сожалению, никогда не понять.
michael_v89
29.11.2024 08:22Я уже объяснил, почему не понять. Потому что когда доходит до дела, вы начинаете игнорировать неудобные вопросы и уходить от ответа. Если так не делать, то понять будет несложно.
Раз вы второй раз ссылаетесь на дискуссии, то прокомментирую. Дискуссия ранее у нас была только одна, в моей статье про логику в сервисах. Вы там привели 4 примера кода.
В 1 мы обсуждали абстрактный вопрос что такое поведение объекта, он не показывает достоинства и недостатки обсуждаемых подходов.
В 2 вы использовали некий Outside, которого нет в бизнес-требованиях. Я указал, что это не соответствует Ubiquitous language и уменьшает понятность кода.
В 3 вы использовали Transactional Outbox, на что я указал, что его можно использовать и в моем подходе.
В 4 вы написали какой-то посторонний метод на добавление продукта, который удобен вам и которого в бизнес-требованиях не было. Это единственный пример, который напрямую относится к основной теме обсуждения.Обсуждения закончились вашими фразами:
"Не вижу смысла обсуждать такие тривиальные вещи как слой UI"
"Если для вас все это является критическими недостатками, используйте свой подход."
"Ок, вы не понимаете преимущества ограниченных контекстов и похоже мне не удастся до вас эту информацию донести"Мое предложение в статье и в комментариях написать код приложения по указанным бизнес-требованиям и сравнить вы проигнорировали. Видимо потому что понимаете, что код в вашем подходе будет выглядеть сложнее.
mike_shapovalov
29.11.2024 08:22Видимо потому что понимаете, что код в вашем подходе будет выглядеть сложнее.
Единственное что я понимаю это то что дискуссия с вами, это пустая трата времени, постараюсь не забыть об этом в следующий раз.
AlexViolin
29.11.2024 08:22Пока просматривается одно решение - алгоритм реализовать в доменном сервисе, а агрегат использовать, как хранилище исходных и расчётных данных алгоритма. И тогда обращение к базе данных за очередной порцией справочных данных будет идти из доменного сервиса. В этом случае агрегат может остаться "вещью в себе" и не содержать в себе никаких зависимостей от внешней инфраструктуры.
mike_shapovalov
29.11.2024 08:22Мне не совсем понятно почему внешнюю зависимость нельзя внедрить в агрегат, но можно внедрить в доменный сервис, который находиться в том же слое. Вместо этого предлагается откатится к анемичной модели т.е. де факто к процедурному программированию, потеряв при этом все преимущества ООП.
AlexViolin
29.11.2024 08:22Конечно зависимость можно внедрить в агрегат, но тогда он перестанет быть "вещью в себе". А автора ddd парадигмы настаивают, что это неправильно и агрегат не должен зависеть от внешней инфраструктуры. И мне такой подход тоже кажется правильным. В этом случае нет другого варианта, как внедрить зависимость в доменный сервис.
mike_shapovalov
29.11.2024 08:22Он и не будет зависеть от инфраструктуры, это инфраструктура будет зависеть от него, для этого и придуман принцип Dependency inversion
mike_shapovalov
29.11.2024 08:22Авторы DDD говорят о том что доменный слой не может напрямую зависеть от слоя инфраструктуры. И сервис и агрегат находятся в слое домена. Но вы почему-то допускаете внедрение зависимости в один из компонентов слоя, но не допускаете в другой.
AlexViolin
29.11.2024 08:22Это пример того как теория может не стыковаться с практикой. Если все нужные справочные данные можно передать в слой домена из вышележащего слоя до запуска алгоритма расчёта, то теория DDD согласуется с практикой. Если же по ходу расчёта в алгоритм надо подтянуть новую порцию внешних данных, то придётся внести внешнюю зависимость или в агрегат или в доменный сервис. Мой выбор - внести зависимость в доменный сервис.
mike_shapovalov
29.11.2024 08:22Я не понимаю как инверсия зависимостей между слоями противоречит теории DDD. И чем внедрение зависимости в сервис лучше внедрения зависимости в агрегат. Напротив, в таком подходе я вижу одни недостатки: либо мы скатывается к анемичной модели и процедурному программированию, либо наш алгоритм размазывается по двум классам, что затрудняет его понимание, поддержку и тестирование.
AlexViolin
29.11.2024 08:22Если алгоритм при расчёте не требует подтягивания новой порции внешних данных, то такой алгоритм можно реализовать внутри агрегата.
mike_shapovalov
29.11.2024 08:22С моей токи зрения алгоритм который требует внешних данных также должен быть реализован внутри агрегата. В сервисе нужно реализовывать только тот алгоритм который изменяет состояние нескольких агрегатов одновременно, да и в этом случае можно обойтись без сервиса, хотя это будет значительно сложнее. Единственным ограничением реализации алгоритма внутри агрегата является то, что такой алгоритм может только получать данные из внешних источников, но не изменять их состояние. Изменять агрегат может только свое состояние.
michael_v89
29.11.2024 08:22Мне не совсем понятно почему внешнюю зависимость нельзя внедрить в агрегат, но можно внедрить в доменный сервис
Потому что доменный сервис обычно создается на старте приложения и существует в одном экземпляре, а сущность создается в середине выполнения запроса по данным из базы, и количество экземпляров может быть хоть 100 штук. Пробросить зависимость в сервис легко, а в сущность нет.
С логической точки зрения - в бизнес-требованиях при описании логики действия используются термины "заказ" и "товар", а не "этот". Поэтому если мы хотим создать правильную модель описанной логики, в коде метода с реализацией этой логики должны быть переменные "order" и "product", а не "this".
mike_shapovalov
29.11.2024 08:22Потому что доменный сервис обычно создается на старте приложения и существует в одном экземпляре, а сущность создается в середине выполнения запроса по данным из базы, и количество экземпляров может быть хоть 100 штук. Пробросить зависимость в сервис легко, а в сущность нет.
При наличии соответствующих инструментов никакой разницы вообще нет.
Поэтому если мы хотим создать правильную модель описанной логики, в коде метода с реализацией этой логики должны быть переменные "order" и "product", а не "this".
Угу, в доменных сервисах у вас по видимому приватных методов с this тоже нет :)
michael_v89
29.11.2024 08:22никакой разницы вообще нет
Ну как это нет. И другому программисту сложнее разбираться, что это за аргумент такой или свойство сущности, которого нет в бизнес-требованиях, и сам вызывающий и вызываемый код усложняется, и на производительность 100 лишних ссылок больше влияют, особенно если их инициализировать этими инструментами через рефлексию.
приватных методов с this тоже нет
Есть, но в данном случае использование this не является частью бизнес-логики, его можно игнорировать и обращать внимание только на название переменных и методов. А в вашем подходе this представляет какую-то сущность из бизнес-требований. Другому программисту надо разбираться, какая часть кода относится к логике, а какая к деталям реализации.
mike_shapovalov
29.11.2024 08:22Ну как это нет. И другому программисту сложнее разбираться, что это за аргумент такой или свойство сущности, которого нет в бизнес-требованиях,
Но если такой аргумент появится в "доменном сервисе" все сразу станет понятно.
и на производительность 100 лишних ссылок больше влияют, особенно если их инициализировать этими инструментами через рефлексию.
Давайте тогда ORM тоже не использовать, некоторые из них тоже рефлексию используют и ссылки на другие объекты в сущность добавляют
Другому программисту надо разбираться, какая часть кода относится к логике, а какая к деталям реализации.
Ага, не дай бог ещё на функцию которая массив фильтрует, или подстроку находит наткнется в бизнес логике, вконец наверное бедный запутается
michael_v89
29.11.2024 08:22Но если такой аргумент появится в "доменном сервисе" все сразу станет понятно.
Так он там не появится, он передается в конструктор сервиса отдельно от сущности, а не в аргументах метода.
Давайте тогда ORM тоже не использовать
От ORM отказаться нельзя без значительных сложностей, а от логики в сущностях можно.
Ага, не дай бог ещё на функцию которая массив фильтрует
Эта функция находится на другом уровне абстракции. Если она будет передаваться в аргументах, или тем более будет среди свойств сущности "createdAt, updatedAt, filterFunction", то да, тоже будет непонятно.
Да, кстати, вы можете использовать такую функцию в сущности только потому что это глобальная зависимость, которую предоставляет рантайм языка. А представьте как будет выглядеть сущность, если все такие зависимости надо будет передавать снаружи, включая объект с методами add/sub вместо "+" и "-". А с сервисом они будут спокойно передаваться в конструктор отдельно от сущностей и их свойств.
mike_shapovalov
29.11.2024 08:22Понятно, с вашей точки зрения любой код с любыми зависимостями в "доменных сервисах" априори будет более понятен с точки зрения бизнес процесса, чем такой же код внутри сущности. Из опыта предыдущих дискуссий с вами знаю, что никакие аргументы вас не переубедят, поэтому дальнейшую дискуссию считаю бессмысленной.
michael_v89
29.11.2024 08:22Так я же написал критерий понятности - находятся ли технические зависимости вперемешку с бизнес-терминами или отдельно. В сервисах они отдельно. Технические зависимости в конструкторе, бизнес-термины это методы и их аргументы.
знаю, что никакие аргументы вас не переубедят
Я вам говорил, какие аргументы переубедят - напишите код, сравним. Бизнес-требования есть в той статье, где была дискуссия. Я не знаю, зачем надо писать много комментариев, если можно написать код и показать результат.
AlexViolin
29.11.2024 08:22В качестве примера более подробно рассмотрю алгоритм расчёт теплообменника. Расчёт теплообменника идёт в составе блока воздухоохладителя или воздухонагревателя. Кроме того возможно использование разных типов теплоносителей или холодоносителей. Поэтому есть множество алгоритмов расчёта. Некоторые алгоритмы состоят из более чем 400 формул. Каждый алгоритм распадается на ветви и далее по ходу расчёта ветвь распадается на подветви. При прохождении алгоритма по любой из ветвей используется не менее 200-250 формул. В алгоритме используется множество промежуточных переменных, которые не имеют никакого отношения к агрегату. Поэтому возникает идея полностью разделить между собой объекты агрегата и алгоритма, и объект алгоритма инжектировать в агрегат через интерфейс. Объект алгоритма по ходу расчёта обращается к базе данных для получения справочных данных. Агрегат не зависит от внешней инфраструктуры - внешних источников данных.
AlexViolin
29.11.2024 08:22Алгоритмы, используемые в доменной логике, можно разбить как минимум на две группы. В первой группе алгоритм встроен в код агрегата. Агрегат создаётся в каком-то первичном состоянии. Затем при обработке внешнего события запускается алгоритм и переводит агрегат в новое состояние. Алгоритм расчёта теплообменника относится к другой группе. Этот алгоритм используется внутри доменного сервиса и до запуска алгоритма агрегат вообще не существует. При запуске алгоритма в него передаётся набор начальных данных для расчёта теплообменника. В общем случае алгоритм рассчитывает целый набор агрегатов-теплообменников, который отображается на визуальной форме. Пользователь из набора выбирает один агрегат и с ним идёт дальнейшая работа в соответствии с используемой бизнес-логикой.
sshmakov
В вашем примере функция считала сверхурочное время сотрудника. Каким образом можно было ранее предвидеть, что это время может считаться различным образом с точки зрения отдела кадров и с т.з. бухгалтерии?
Помимо "один компонент отвечает за что-то своё", в S входит тезис "что-то свое должно быть полностью инкапсулировано в компонент". Применяя к вашему примеру, почему ответственность за расчет сверхурочного времени должна быть размыта между разными компонентами?
E_STRICT
Я думаю это был не удачный пример. Функция изначально вовсе и не нарушала Приницип Единственной Ответственности.
nin-jin
Я бы даже сказал, что это удачный антипример. И SRP тут, конечно же, нарушается. Просто принцип бестолковый. Что и демонстрирует данный пример. Бухгалтерия должна была считать сверхурочные по новым правилам, но любитель копипасты тут сделал кривую архитектуру, разделив то, что должно быть связано.
Vladislav_Dudnikov
Про (1). Конечно нельзя заранее предугадать, что расчёты будут разные. Поэтому изначальное решение не ошибка, а гипотеза разработчика, что бухгалтерия и отдел кадров - одно действующее лицо (ДЛ, actor), причём решение почти точно согласованное с бизнесом. Со временем требования меняются и вполне допустимо, что ДЛ разбивается на два. Тут главная мысль в том, что нужно уловить момент, когда идёт разбиение на два ДЛ, а когда нет (что не всегда можно сделать).
mvv-rus
Принцип Единой Ответственности плох тем, что он операется на зыбкое, точно не определеннное понятие "ответственность". Можно к примеру, сделать объект "клиент", который отвечает за все операции, касающиеся клиента - и вот вам "божественный объект", который формально SRP соответствует, но запутанность (coupling) внутри приложения создает ого-го какую! Причем, эффект от этой запутанности может быть разным: в маленьком приложении ей можно принебречь, а вот в большом, расползшемся на разные области с разными взавимодействиями с клиентом, все прелести хрупкости/жескости/неподвижности встанут в полный рост.
И то, что ответственность каждый может понимать по-своему - это ещё пол-беды. Беда в том, что правильное, т.е. адекватное задаче приложения, понимание меняется вместе с развитием приложения. Как здесь: пришла новая нормативка по подсчету - и единая ответственность внезапно разделилась на две. Придет другая нормативка - например, по разнице подсчета средней продолжительности рабочего месяца в невисокосном и високосном годах - ответсвенность ещё поделится на две, и вот их уже четыре.
То есть, сам принцип SRP требует для своего применения творческого (и я сказал бы - диалектического, см. ниже) подхода.
Конечно, кто-нибудь (например, автор статьи) может мне сказать, что предметно-ориентированныое проектирование спасет от этой беды, но ведь клиент-то реально один, и как его поделить на ограниченные контексты - это вопрос именно что диалектический, потому как упирается в эту самую диалектику как учение о всеобщей связи и зависимости, которой наше поколение некогда пытались научить (получалось, правда, обычно плохо).
То есть вся эта наука, про которую тут пишет автор статьи - это субстанция зыбкая, требует не только конкретизации по месту, но и предвидения, куда будет развиваться конкретная проектируемая система. То есть, сама по себе она фундаментом служить не может, а требует для своего применения людей, которые будут применять ее творчески в конкретных условиях и пользуясь своим невербализуемым опытом. Что эта наука может дать - так, во-первых, общий язык, а во-вторых - эврстики, которыми можно пользоваться для выбора решения. Но можно и не пользоваться - правильный выбор, по-любому, остается за человеком, творцом.
И из этого есть ещё кое-какие следствия, но об этом - как нибудь, в другой раз (статью, что ли написать :-) ).
DasMeister
Одна из ключевых проблем SOLID в том, что все забывают зачем он нужен - для создания систем, которые будут динамически развиваться и поддерживаться.
SRP влияет на разные уровни абстракции - реализации метода, компоновки поведения (класс и объект), сервисные сценарии, приложения (монолиты, микросервисы), системные архитектуры и их компоненты.
Когда мы используем SOLID, мы создаем компоновку кода такой, чтобы она умела реагировать на изменение событий. И в данном случае уровень влияния SRP должен сместится выше - к объекту, а изменение должно соответствовать при этом буквально следующей литере в акрониме - OCP.
Сам по себе SRP вполне себе ясный инструмент акронима, беда в том, что вместо понимания и восприятия идей, начинается догматизм в попытке выяснить, что значит слово Responsibility. В то время как динамические идеи вообще плохо догматизируются, они нужны как инструмент мышления и оценки.
Задача SOLID в целом, позволить сформировать дизайн, который будет эволюционировать по ходу существования системы, а не дать ответы на все вопросы в камне прямо сейчас. И SRP один из примеров - т.к. Мартин определял его на протяжении 20 лет по разному для тех, кто пытался молиться на акроним. Это бесполезно, суть сборника идей в их адаптации, а не в побуквенной диалектике.
LehaKos
Ну Мартин объясняет это тем, что многие неправильно понимают этот принцип. Он говорит, что у компонента должна быть только 1 причина для изменения. Если функция используется двумя разными отделами, то требования для изменения будут приходить из 2х источников. Значит принцип единственной ответственности нарушается.
mvv-rus
По вашим словам получается, что понятие "ответственность" лежит за пределами свойств собственно ПО, а определяется организационными и бюрократическими причинами. Я вас правильно понял?
Если так, то там опять-таки возникает неопределенность в понимании, где именно границы ответственности, и опять-таки границы могут поменяться в процессе эксплуатации.
k4ir05
Именно так Мартин это и объясняет. Речь не про ответственность класса (модуля), а про ответственность за класс (модуля). В пределах ПО для этого и так хватает принципов - low coupling и high cohesion, например.
Там же, где и должностные, наверное.
И это нормальный естественный процесс. ПО тоже меняется.