Pull to refresh

Comments 40

DDD - это таинственный зверь, про которого пишут тысячи статей, пересказывая вновь и вновь одно и тоже из статьи в статью про Bounded Contexts, Aggregates, Ubiquitous Language и рисуя схемы гексагональной архитектуры, но при этом никто этого зверя в глаза не видел и не горит желанием выложить реальный проект с реальным решением проблем через DDD, где будет видно что ничего из этого в реальной жизни не работает, ну или наоборот все оценят и поймут как это круто и не нужно будет дальше писать тысячи статей. Но нет зачем что-то реализовывать, лучше про это писать.

-У тебя есть карта реальный пример проекта?

-Лучше! У меня рисунок карты гексагональной архитектуры.

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

Это не совсем так. DDD — не "волшебная таблетка", а набор практик для сложных доменов, где бизнес-логика запутана и меняется. Его редко применяют "по учебнику", потому что:

  • Каждый домен уникален, и решения адаптируются под контекст.

  • Реальные примеры часто закрыты NDA (банки, и тд).

Пример открытых проектов:

  • Домены в системах типа Apache Isis (хотя это уже "тяжелая артиллерия").

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

-У тебя есть карта реальный пример проекта?

-Лучше! У меня рисунок карты гексагональной архитектуры.

Схемы - это лишь инструмент для визуализации. Суть в том, чтобы:

  • Изолировать доменную логику от инфраструктуры (базы, фреймворки)

  • Упростить тестирование

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

Согласен, что вокруг DDD много шума. Но его ценность - не в модных терминах, а в фокусе на смысле, а не на технологиях. Если у вас есть примеры, где DDD не сработал — давайте обсудим! Мне самому интересно, где границы его применимости. Именно поэтому я и писал эту стать. Именно поэтому я и рисовал красивые графики. И именно поэтому я отвечаю подробно ваш комментарий.

Если у вас есть примеры, где DDD не сработал — давайте обсудим!

У меня есть.

Так-то, эта статья тоже весьма и весьма поверхностна. Просто список терминов и ваше короткое мнение., без реальных примеров или обоснования.

Это не ddd хайпует, а вы хайпуете на ddd)

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

Однажды научившись писать "хорошо" сложно смотреть на спагетти код коллег по цеху, жирные контроллеры, размазывание бизнес логики по всем уголкам проекта, тот же mvc и тд.

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

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

Отношусь к DDD как к методу обменять бизнесовую сложность на техническую.

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

Самый сложный тактический паттерн — агрегат. Выбрать правильные границы с первого захода очень непросто. Да и ORM'ы, к сожалению, далеко не всегда способны в маппинг по-настоящему, когда объекты не являются копиями плоских таблиц в памяти.

как к методу обменять бизнесовую сложность на техническую

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

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

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

FP нормально обходится без rich объектов, но там обычно получше с системами типов. https://pragprog.com/titles/swdddf/domain-modeling-made-functional/

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

Если код не является хорошей моделью релевантных бизнес понятий и процессов

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

Так никто ж не предлагает вообще все в класс тащить. Для этого и существуют доменные события, доменные сервисы и сервисы уровня приложения.

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

Чисто из недавнего пример:

Нужно сохранить номер заказа во внешней системе, разработчик влепил:

public string? OrderReference {get; set;}

Но можно чуть иначе:

public string? OrderReference {get; private set;}

public Result LinkOnceToOrder(string orderReference)
{
    if (OrderReference is not null && orderReference != OrderReference)
    {
       return Failure();
    }
  ...
}

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

В принципе, можно и для OrderReference, при необходимости, свой тип определить или, если поведение сущности меняется, разбить тип самой сущности на два: Xyz и LinkedXyz (имена условные).

Я скорее про случаи когда бизнес объект вырождается в DTO с кучей мутабельных свойств типа int или string.

Ну и я про эти случаи.

И совершенно неочевидно, как эти свойства эволюционируют и взаимодействуют меж собой.

Открываете соответствующий метод сервиса, там всё написано.
Как свойства эволюционируют и взаимодействуют, задается бизнес-требованиями, вы же их при анализе бизнес-требований и извлекаете.

Но можно чуть иначе
LinkOnceToOrder

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

Если LinkOnceToOrder вызывается при создании, то и при прямом доступе OrderReference будет устанавливаться один раз, потому что создание заказа вызывается только один раз. Нет необходимости это отдельно проверять.

Линкануть можно единожды

А зачем? Если у вас не было такого требования от бизнеса или хотя бы от разработчиков внешней системы, то и в вашем коде его быть не должно. Вы же говорите про правильную модель.
Потом приходят разработчики из внешней системы и говорят "У нас там был сбой, мы вам неправильные номера заказов отправляли, перезапишите новые из вот этого эндпойнта".

Сервисов может быть 100500 и у сервиса по 100500 методов. Если каждый позволяет себе писать в свойства объекта, то инварианты придется собирать по всей системе, а также будет тяжело гарантировать их сохранность.

Если сервис А пишет в свойства X и Y, а сервис B в свойства Y и Z, то если существует правило для свойств X и Z, вы рискуете его нарушить. На практике ещё хуже, сначала правила нет, а потом вдруг нарисовывается. Заниматься поиском всех мест, где оно вовлечено, занятие неблагодарное.

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

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

Сервисов может быть 100500 и у сервиса по 100500 методов.

Ну значит у бизнеса столько бизнес-сценариев. Значит он имеет свои правила, как они должны работать вместе.

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

Зачем вам их собирать? Инварианты есть у конкретного бизнес-процесса, у другого процесса свои инварианты. Бизнес сам разберется, чтобы его требования были непротиворечивы. Вам надо только создать их правильную модель в коде.

Некоторые инварианты вообще нельзя гарантировать на уровне одного объекта сущности. Например, нельзя гарантировать правило "Пользователь не должен иметь более 3 статей на модерации", имея доступ только к полям одного объекта Article. Это инвариант процесса "Отправка на модерацию". При этом когда модератор хочет перевести уже опубликованную статью обратно на модерацию, и она становится 4-й для автора статьи, то это должно быть разрешено.

то если существует правило для свойств X и Z, вы рискуете его нарушить.

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

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

Ну она и минимальна, связь задается правилами бизнес-процесса, бизнес-процесс описан в методе сервиса, поэтому связь описана максимально близко к бизнес-процессу.

В требованиях нет кейсов на изменение или сброс ссылки.

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

В ФП вообще нет понятия "ответственность". Нет и путаницы, какой объект отвечает за это.

А вы в курсе сколько штраф по договору за такие исходники?

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

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

Пока думаю единственное что может быть полезным из DDD это ограниченный контекст.

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

  • Единый язык (Ubiquitous Language) снижает риски недопонимания между разработчиками и бизнесом

  • И отдельно добавлю про Bounded Contexts, который поможет дробить монолиты на микросервисы

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

Я вот выбирал как-то из 3х вариантов. Тут нет полного ответа, но логика понятна:

https://LLMshare.syntxai.net/bf727f63-55f9-d062-1f2817af

А потом даже экономическое обоснование выбора сделал.

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

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

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

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

Есть ли четкая формула успеха?

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

Так что только экспериментальным итеративным образом с примесью интуиции и отсебятины.

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

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

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

Я имею в виду, что "использование BC" != "легкое дробление мололита на микросервисы". Может облегчить - да, но ровно настолько же, сколько в целом модульный подход к дизайну, в том числе и в инфраструктуре.

Работаю на проекте, где сейчас в одном из модулей внедрен ддд

Цель ясна - упростить разработку и поддержку, разделить на слои и все такое

Вот только небольшие правки выливаются в МР с 20 файлами из-за чудовищного количества абстракций

Единый язык (Ubiquitous Language)

Говорите на одном языке с бизнесом. Если бизнес говорит "клиент", то и в коде должно быть "клиент", а не "user" или, что ещё хуже, "entity". Это кажется очевидным, но вы удивитесь, если чекните свои репозитории, можете найти, что client = user = human

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

Если очень хочется пиши словарь-карту бизнес термин-переменная в коде

Расхотел?

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

Ну что ж, бывает когда бизнес термин гуляет. Бывает даже, когда основные компетенции в бизнес процессе не у самого бизнеса а в ИТ

Да, по сути, когда бизнес говорит "пользователь", "покупатель", "лид" он может подразумевать одну и ту же личность; но в разных контекстах эта личность выступает в разных ролях. Вот в DDD для этого принято решение создавать отдельный инстанс данных под роль, отражая факт корреляции с одной и той же личностью в ID или каких-то схожих свойствах.

Или вы говорите именно про 1С и про то, что там принято выражать все эти проекции в одной сущности?

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

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

Странно почему вы не упомянули EDA как практическое применение DDD. К слову весь хайлоад (правильный) работает на EDA. Так что вопрос достаточно спорный о том существует ли в природе DDD или нет. Кроме того именно благодаря DDD появляются такие штуки как SQRS, CDC, Саги и многое другое.

Ну и даже во фронтенд этот угрюмый React- Redux по сути реализация DDD через EDA. Как бы мы не ненавидели Redux он до сих пор скорее всего самый распространенный шаблон проектирования фронтенд приложений.

EDA работает и без DDD. А может и вместе с DDD. CDC, Саги отношения к DDD не имеют. С остальным согласен 🙂

По-моему, когда люди говорят, что "на проекте был внедрен ДДД", это уже значит, что они не понимают, о чем говорят. Не удивительно, что им все кажется переусложненным и запутанным.

ДДД - не фреймворк и не пакет, который можно подключить или отключить. Это набор "бест практик", которые можно использовать хоть по одной, хоть все вместе, хоть во всем проекте, хоть в отдельных его частях. Если, конечно, их правильно понимать. Некоторые языки помогают его использовать (например, поддерживая неизменяемость из коробки), в других приходится костылить. Возможно, из-за последнего некоторым кажется, что код усложняется.

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

Вообще не понимаю как сделать DDD без костылей, DDD предполагает, что у вас будет логика в домменой модели, логике нужны зависимости, а вы не хотите, чтобы модель с данными имела зависимости, так как ими сложно управлять. В итоге в своей работе я использую следующий подход, есть модели отражающие таблицы в базе данных (Entities) в них только поля для данных и никакой логики. Есть модели отражающие объекты API (DTO) так же не содержат логики, только данные. Есть обработчики, суть которых выполнять бизнес-логику (Handlers) содеражат только один единственный метод Handle, мы используем MediatR, но со следующими ограничениями медиатор может вызвать только метод контроллера (mvc) и никто другой, собственно единственное что может вызвыть метод контроллера - это медиатор, больше он ничего сделать не может, ни из каких других мест медиатор вызвать нельзя, по сути сейчас есть fast endpoints можно заменить mvc и MediatR на них. Есть сервисы для общения с внешними системами (Services) содержат логику, не содержат данных. Ну и если есть сложная, не очевидная логика, которая повторяется в нескольких обработчиках, то она тоже выносится в сервис и сервис прокидывается в обработчик как зависимость. Таким образом удаётся писать код, который легко читать, легко поддерживать, в котором редко возникают git конфликты при работе нескольких разработчиков, а решать конфликты довольно просто.

Просто спроси deepseek “объясни мне, словно ты системный архитектор, в чем отличие DDD применимо к архитектуре системы в целом, от DDD применимо к коду системы” и мир станет чуточку понятнее

Отличный вопрос! Давай разберёмся, как Domain-Driven Design (DDD) работает на разных уровнях системы:

1. DDD для архитектуры системы (макроуровень)

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

Ключевые аспекты:

  • Ориентация на домен – система разбивается на ограниченные контексты (Bounded Contexts), каждый из которых отвечает за свою часть бизнес-логики.

    • Пример: В системе интернет-магазина могут быть контексты "Заказы", "Доставка", "Оплата", "Каталог".

  • Способы взаимодействия между контекстами:

    • Shared Kernel (общее ядро) – если контексты используют общие модели данных.

    • Customer-Supplier (поставщик-потребитель) – один контекст явно зависит от другого (например, "Оплата" зависит от "Заказы").

    • Conformist (конформист) – один контекст подстраивается под модель другого.

    • Anticorruption Layer (ACL) – защитный слой, который изолирует систему от чужой модели (например, при интеграции с внешним API).

  • Микросервисы vs Монолит

    • В микросервисной архитектуре Bounded Context часто соответствует отдельному сервису.

    • В монолите – это логический модуль (но может быть и физически выделенным).

Пример:

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

  • Bounded Context "Грузоперевозки" – отвечает за маршруты, транспорт, водителей.

  • Bounded Context "Склад" – управление запасами, приемка/отгрузка.

  • Способ взаимодействия:

    • "Грузоперевозки" запрашивают у "Склада" данные о наличии товара через API с ACL, чтобы не зависеть от его внутренней модели.

2. DDD для кода системы (микроуровень)

Здесь DDD — это способ структурирования кода внутри одного Bounded Context, чтобы он отражал бизнес-логику.

Ключевые аспекты:

  • Слоистая архитектура (но не всегда!):

    • Domain Layer – ядро, где живут сущности (Entities), агрегаты (Aggregates), доменные сервисы (Domain Services), value-объекты (Value Objects).

    • Application Layer – координирует выполнение сценариев использования (Use Cases), но не содержит бизнес-логики.

    • Infrastructure Layer – реализация репозиториев, работа с БД, внешними API.

  • Паттерны проектирования внутри контекста:

    • Aggregate Root – главная сущность, через которую идёт всё взаимодействие (например, Order в контексте заказов).

    • Repository – абстракция для доступа к данным (например, OrderRepository).

    • Domain Events – события, которые порождает домен (например, OrderShippedEvent).

    • Factory – создание сложных объектов.

Пример:

В том же Bounded Context "Грузоперевозки" код может выглядеть так:

// Domain Layer
class Cargo { // Aggregate Root
    private CargoId id;
    private Route route;
    private DeliveryStatus status;

    public void assignDriver(Driver driver) { ... } // бизнес-логика
}

// Application Layer
class CargoBookingService {
    private final CargoRepository repository;

    public void bookCargo(CargoDetails details) {
        Cargo cargo = CargoFactory.create(details);
        repository.save(cargo);
    }
}

Главные отличия:

Аспект DDD для архитектуры (макро) DDD для кода (микро) Фокус Разделение системы на контексты Организация кода внутри контекста Основные понятия Bounded Context, ACL, интеграции Агрегаты, сущности, репозитории Масштаб Вся система, сервисы, API Классы, методы, бизнес-правила Инструменты Event-Driven, REST/gRPC, схемы API Паттерны (Aggregate, Factory и т.д.)

Вывод:

  • Архитектурный DDD помогает разделить систему на логические части и правильно организовать их взаимодействие.

  • Кодовый DDD помогает написать чистый, выразительный код, который точно отражает бизнес-логику.

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

Sign up to leave a comment.

Articles