Давление сроков и реалистичные ловушки проектирования домена
При начале нового проекта многие разработчики, хотя и понимают разумом ценность проектирования, ориентированного на домен (DDD, Domain-Driven Design), иногда выбирают практические компромиссы. Особенно в условиях давления сроков, требующих быстрого выхода на рынок, сложно потратить много времени на тонкое моделирование домена.
Проект платформы управления рабочей силой также пытался с самого начала разработки писать бизнес-код, основываясь на принципах DDD. Однако из-за сжатых сроков разработки, когда мы шли к быстрому проектированию домена, мы упустили ключевые связи, важные с архитектурной точки зрения.
Ограничения проектирования, с которыми мы столкнулись в тот момент, можно выделить на три больших группы.
-
Отсутствие четкого определения границ агрегатов домена (Aggregate)— Не удалось четко определить, какие жизненные циклы и области транзакций должны разделять отдельные объекты домена.
-
Бездумное превращение объектов значений (Value Object) в сущности (Entity)— Даже объекты значений, для которых не требуется идентификатор и которые должны быть представлены просто как набор свойств, были спроектированы как независимые сущности без особой необходимости.
-
Недостаток установки отношений между доменами— Мы не смогли установить явные отношения на уровне домена, которые описывают, как объекты связаны и взаимодействуют между собой.
В таком состоянии, когда добавлялись бизнес-правила, сложность системы начала резко возрастать. Поскольку отношения между доменами не были заявлены в коде, слой сервиса (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