golang

Структура кода в папке Domain по DDD

  • суббота, 13 декабря 2025 г. в 00:00:09
https://habr.com/ru/articles/975936/

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

Структура и язык кода должны соответствовать бизнес-домену

Давайте посмотрим на базовую структуру папки Domain

В данной главе огромная возможность написать много текста, почему следующие два примера плохи с точки зрения OOD, Cohesion/Coupling, расширяемость, сопровождение, сложность изучения кодовой базы и тд - но я этим заниматься не буду. Об этом есть много информации в сети.

Вариант 1

Domain/
    Entity/
    ValueObject/
    Repository/
    DomainService/
    Event/

Вариант 2

Domain/
    Model/
    Services/
    Repository/

Глядя на эту структуру - первое что приходит в голову - хм... наверное мы все же позабыли пункт про соответствие бизнес-домену и запомнили только про 4 раздел нашей любимой синей книги. А может мы вовсе прочитали только статью/книгу "DDD на языке Х" и даже не имеем мысли о том что здесь что-то не так.

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

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

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

Как же все таки стоит организовать структуру кода по DDD

Первое правило Бойцовского клуба

Первое правило Бойцовского клуба: никому не рассказывать о Бойцовском клубе.

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

Domain/
    Template/
        Attribute\
            Attribute.php
            AttributeType.php
            AttributeValue.php
            AttributeStyle.php
        Template.php
        TemplateChanged.php
        TemplateArchivated.php
        TemplateRepositoryInterface.php
        CalculateRecomendationScore/
          RecomendationScore.php
          CalculateRecomendationScore.php        
          ScoreUncalculatable.php        
    ProductCode.php
    ProductPrice.php
    Product.php
    ProductRepositoryInterface.php
    AssignProductToTemplate/
        MoveProductToTemplate.php
        ProductTemplateChanged.php
        ProductCannotBeAssignedToTemplate.php

Здесь я считаю всё по канону:

Мы видим из чего состоит предметная область нашего контекста. Мы в принципе видим предметную область :D а не группы типов из которых она состоит.

Мы видим что речь о продукте. Мы видим что у продукта есть support-sub-domain Шаблон. Нам не интересно ProductPrice это VO или Entity, нам важно какую бизнес задачу решает этот объект. Мы видим бизнес-фичу "Назначить продукт на шаблон" и все её внутренние состовляющие которые она в себе содержит (событие и ошибку, чаще всего на практике там будет DTO для input/output параметров сервиса).

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

Советы по данной структуре

Свод правил по которым будет строится структура - неописуем. От проекта к проекту он будет разный основываясь на особенностях единого языка и структуры предметной области которую мы реализуем. Для себя я вывел парочку на основе лингвистики и и "здравого смысла".

  1. Фичи (aka доменный сервис) именуем в повелительном падеже: Сделай что-то. Создай товар. Обнови цену. Проверь имя на уникальность. Посчитай очки популярности.

  2. Саб-домены именуем существительными

  3. Не создавайте VO просто так

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

    2. Делайте VO общими и выносите их Shared для типичных вещей. Например PositiveInteger, NotEmptyString (на любителя)

  4. Доменные события именуем в прошедшем времени

  5. Ошибки именуем с действием в отрицательной форме

    1. Вообще ошибка - это результат юз кейса. Если мы этого придерживаемся и не строим свою логику на ошибках - классов с ошибками будет немного. Достаточно будет одного Shared\DomainError.
      Базовый пример это repo->find(): null|Object + repo->get() :Object.
      Не стоит оборачивать get в try catch если вам нужно что-то сделать когда агрегата не существует. Это логика, а для логики есть if. В большинстве случаев работать с ошибками мы будем только на инфраструктурном уровне.

  6. Не бойтесь рефакторинга - это бесконечный процесс если вы не потратили 100500 часов на сеансы EventStorming со своими бизнес экспретами

    1. С течением времени вы будете узнавать свою предметную область все больше и больше. К вам будет приходить понимание того, что все таки этот кусок кода нужно вынести из Foo в Bar и окажется что этот доменный сервис является частью нового саб-домена.

Заключение

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

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