Путешествие от моделирования данных к DDD

Путешествие от моделирования данных к DDD

Давление сроков и реалистичные ловушки проектирования домена

При начале нового проекта многие разработчики, хотя и понимают разумом ценность проектирования, ориентированного на домен (DDD, Domain-Driven Design), иногда выбирают практические компромиссы. Особенно в условиях давления сроков, требующих быстрого выхода на рынок, сложно потратить много времени на тонкое моделирование домена.

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

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

  1. Отсутствие четкого определения границ агрегатов домена (Aggregate)— Не удалось четко определить, какие жизненные циклы и области транзакций должны разделять отдельные объекты домена.

  2. Бездумное превращение объектов значений (Value Object) в сущности (Entity)— Даже объекты значений, для которых не требуется идентификатор и которые должны быть представлены просто как набор свойств, были спроектированы как независимые сущности без особой необходимости.

  3. Недостаток установки отношений между доменами— Мы не смогли установить явные отношения на уровне домена, которые описывают, как объекты связаны и взаимодействуют между собой.

В таком состоянии, когда добавлялись бизнес-правила, сложность системы начала резко возрастать. Поскольку отношения между доменами не были заявлены в коде, слой сервиса (Domain Service) каждый раз должен был вручную восстанавливать связи, получая рассеянные сущности. Естественно, логика сервиса начала разрастаться, и стало сложно проверять согласованность.

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

Установление терминов и понятий: Убиквитарный язык (Ubiquitous Language) в коде

Первая задача DDD — это создание убиквитарного языка (Ubiquitous Language), на котором будут общаться все участники проекта, включая проектировщиков, дизайнеров, разработчиков. Код проекта до рефакторинга использовал технические термины или неопределенные слова в именах объектов домена.

От 'технических слов' к 'словам бизнеса'

Типичным примером была сущность, представляющая действия работника по подаче заявки на трудоустройство в объявлении о вакансии. В исходном коде она была просто названа Application. Хотя это слово было вполне естественным для разработчиков, оно имело следующую проблему.

  • Application смешивалось с техническими терминами, означающими фреймворк или само приложение, что вызывало путаницу.

  • В лексиконе бизнеса по предоставлению рабочей силы более подходящим термином оказалось 'поддержка (Enrollment)', что обозначает намерение зарегистрироваться и работать.

В процессе рефакторинга мы изменили это на название домена Enrollment. Кроме того, мы улучшили названия на более интуитивные с точки зрения бизнеса, такие как EnrollmentProfile и SubmittedDocument, исключив технический термин Snapshot, который временно использовался для файлового резервного объекта ApplicationWorkerProfileSnapshot в процессе проверки заявлений.

[До] Техническая ориентированная номенклатура

JobApplication ──> ApplicationWorkerProfileSnapshot ──> ApplicationDocumentSnapshot

[После] Бизнес-ориентированная номенклатура

Enrollment ──> EnrollmentProfile ──> SubmittedDocument

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

Установка границ агрегации: Преодоление неразумной сущностной модели

На начальном этапе проектирования самым большим долгом были 'объекты, бездумно спроектированные как независимые сущности'. В объектно-ориентированном подходе и DDD сущность должна иметь уникальный идентификатор (Identity) и быть отслеживаемой на протяжении всего жизненного цикла. В то время как объекты значений (Value Object / Value Group) представляют собой просто свойства без идентификатора и зависят от жизненного цикла родительского объекта.

Из-за дефицита времени, пытаясь реализовать все в виде табличных сущностей, каждая сущность получила свою независимую таблицу, репозиторий и логику. Это привело к чрезмерному количеству join'ов в базе данных и экспоненциальному росту затрат на поддержание согласованности.

Примеры JobPost и JobPostRecruitment

Типичными примерами являются JobPost, содержащий информацию о вакансии, и JobRecruitment, который отображает конкретные требования по набору персонала и ставкам в пределах этого объявления. Изначально они были полностью зависимыми подконцепциями одного объявления, однако в начальном проектировании были реализованы как независимые StageEntity. Это вызвало следующие неэффективности.

  • При редактировании объявления о вакансии, для изменения данных об условиях найма нужно было отдельно извлекать и редактировать сущность JobRecruitment в рамках транзакции.

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

В процессе рефакторинга мы понизили JobRecruitment с независимой сущности до группы объектов со значением (Value Object) без идентификаторов, названной ValueGroup. И это было полностью инкапсулировано внутри корня агрегата JobPost.

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

Установление отношений между доменами и оптимизация слоя сервисов

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

[Форма первоначальной сервисной логики]

1. Извлечение сущности A по ID

2. Ситуация требует внешнего идентификатора сущности B, но связи нет, поэтому напрямую извлекаем определенное поле сущности A

3. На основе извлеченного поля отдельно выполняем запрос к БД для получения сущности B

4. Сущности C также собираются вручную таким же образом для обработки логики

Если настройки отношений между доменами (например, @FieldSourceId, внешние ключи и соглашения о связанных полях) неполные, сущности рассыпаются подобно независимым песчинкам. В таком случае, чтобы завершить логику, слою сервисов приходится выполнять тяжелую работу.

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

Установление отношений и переход к богатой доменной модели

Мы четко переустановили внешние ссылки идентификаторов и отношения на уровне модели между доменами. Указали связанные внешние ключи для каждой сущности и явно определили необходимые подассоциации внутри сущности. Кроме того, мы заблокировали злоупотребление сеттерами, чтобы нельзя было произвольно изменять поля извне сущности, а модификация состояния объекта осуществлялась исключительно через четко определенный метод действия modifyAttributes.

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

Вдохнуть жизнь в домен с помощью кода

Процесс рефакторинга домена в этом проекте не был просто упорядочиванием, состоящим в изменении имен столбцов базы данных или структуры пакетов. Это было "смена перспективы, с которой система смотрит на бизнес".

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

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

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

  • Предсказуемость и стабильностьЧерез инкапсуляцию и строгий дизайн ID, а также изоляцию агрегатов неожиданные побочные эффекты были полностью устранены. Система стала гораздо более предсказуемой и надежной.

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

informalife

Site footer