Как стать автором
Обновить

Domain-Driven Design: ошибки, которые не описаны в книгах

Уровень сложностиСредний
Время на прочтение13 мин
Количество просмотров4.7K

Всем привет! Меня зовут Андрей, уже несколько лет я работаю тимлидом/техлидом в разных компаниях и различных проектах. В последнее время подход Domain Driven Design у всех на слуху. Хотя этот подход развивается уже много лет (с 2003), только сейчас на него обращают активное внимание и многие команды пробуют внедрять его у себя.

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

Сначала краткое введение о том, что из себя представляет Domain Driven Design.

Что такое DDD

Domain-Driven Design (DDD) — это подход к разработке ПО, впервые предложенный Эриком Эвансом в книге "Domain-Driven Design: Tackling Complexity in the Heart of Software" (Eric Evans: "Domain-Driven Design: Tackling Complexity in the Heart of Software").

Основной смысл данного подхода — это моделирование бизнес-домена, то есть создание программной модели, которая отражает бизнес-процессы и правила предметной области.

Цели подхода DDD

  • Фокус на бизнес-ценности: При разработке программных систем мы уводим фокус с технологий и фреймворков и моделируем необходимые бизнес-процессы.

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

  • Отражение реального домена: В идеальном случае описание бизнес-процессов, их правил, проблем и целей бизнеса производится непосредственно в коде. Это делает разработку более прозрачной для всех участников процесса разработки.

  • Гибкость и адаптивность: Отделение бизнес-логики от технологий и фреймворков позволяет быстро адаптироваться к изменениям требований заказчика и внедрять новые фичи без значительных рисков.

Ключевые принципы

  • Domain — предметная область, совокупность проблем и целей бизнеса. Предметная область, выраженная в коде, называется Domain Model. Здесь происходит отражение бизнес-логики и моделирование правил и ограничений.

  • Ubiquitous Language — самый важный и ключевой, на мой взгляд, принцип. Единый язык и единая терминология, который применяется повсеместно — в коде, документах, обсуждениях.

  • Bounded Context — определенная область в системе, в рамках которой существует единый язык (Ubiquitous language). Вся система разбивается на ограниченные области (контексты), которые имеют четкие границы между собой.

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

  • Aggregate — это группа объектов, обрабатывающихся как единое целое, объединенных общими характеристиками и поведением. Если говорить в терминах транзакционности, то в рамках одной транзакции существует один агрегат. Агрегат — входная точка для взаимодействия, в рамках которого существуют границы согласованности данных. Также он является единицей атомарности и управляет своим внутренним состоянием.

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

Основные преимущества использования подхода DDD

  • Повышение качества и читаемости кода благодаря четкому разделению зон ответственности.

  • Уменьшение сложности написания тестов на бизнес-логику.

  • Улучшение коммуникации между командой разработки и бизнесом.

  • Уменьшение риска ошибок и несогласованности между частями системы.

  • Упрощение поддержки и расширения функционала.

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

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

Ниже попробуем сформулировать ошибки, которые могут возникнуть при внедрении Domain Driven Design. Определим и опишем признаки, по которым эти ошибки возможно выявить и попытаться исправить еще на этапе их появления. Начнем с фундамента — Ubiquitous Language.

Основные ошибки при формировании Ubiquitous Language

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

Вот несколько грубых ошибок, которые могут сильно "вставить палки в колеса" проекту и вызвать значительные проблемы и переделки систем в будущем:

  • Использование технического жаргона: Вместо терминов предметной области разработчики начинают применять технические термины. Это приводит к разрыву коммуникации между разработкой и бизнесом.

А бывает и наоборот, в своем опыте я встречал такие ситуации — бизнес настолько привыкает к техническим жаргонизмам, что и сам начинает употреблять их в повседневной жизни. И вместо того, чтобы говорить, например: "Реализуйте мне фичу по расчету экономических показателей", начинает требовать — реализовать микросервис (!) по расчету экономических показателей. Эта ситуация, на мой взгляд, еще опаснее — бизнес начинает непосредственно влиять на проектирование архитектуры систем, не понимая основ проектирования программных систем.

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

В одном из проектов у нас существовал термин "тикет" (да, на русском :)), который в одном случае означал посадочный талон, в другом — подтверждение оплаты за билет, в третьем это был билет, у которого был определенный жизненный цикл, включающий некоторые из описанных (не понимаю, из описанных чего?). Это все приводило к безумной путанице при разработке.

  • Отсутствие единой терминологии: Разные команды или даже члены одной команды используют разные термины для одних и тех же понятий.

В моей практике недостаточное внимание к терминологии вылилось в двойное толкование одного и того же термина. Product owner постоянно путался в терминах сущностей, уже позже мы поняли, почему: как оказалось, мы недостаточно хорошо проработали язык, возникло двойное толкование одной и той же сущности, вследствие чего мы неправильно поделили систему на ограниченные контексты, из-за этого разбили функционал на два микросервиса, когда там нужен был один. У нас возникла циклическая зависимость между сервисами. И только потом мы поняли, почему наш Product owner путался в терминах — это была одна сущность, но было уже поздно и пришлось переписывать проблемные части.

  • Изоляция терминов: Термины существуют только в документации, но не используются в реальной коммуникации. Более того, часто в коде они могут называться совсем по-другому.

Признаки проблем с Ubiquitous Language

На что стоит обратить внимание в процессе разработки терминологии? И как понять в процессе разработки, что у нас что-то идет не так? Попытаемся сформулировать признаки, которые могут сказать, что Ubiquitous Language имеет какие-то неточности и требует пересмотра/переработки:

Коммуникационные:

  • Частые уточнения значений терминов на обсуждениях и встречах. Разные члены команды постоянно уточняют, что имеется в виду, когда говорят о тех или иных терминах.

  • Если команда регулярно сталкивается с недопониманиями и спорами по поводу терминов.

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

Организационные:

  • Сложности в обучении новых участников команды. Новые члены команды не могут быстро разобраться в терминологии и концепциях.

  • Разные команды и/или разные члены команды используют разные термины для описания одних и тех же сущностей и процессов.

  • Отставание от бизнес-процессов. Язык не отражает актуальные изменения в бизнесе.

Архитектурные:

  • В документации используются разные термины для описания одних и тех же процессов.

  • Бизнес-эксперты не понимают и не могут прочитать диаграммы и код. Приходится долго объяснять, что имеется в виду.

  • Документация становится запутанной, так как каждая команда интерпретирует одни и те же термины по-своему.

Разработка:

  • Частые конфликты при изменении одной сущности, приводящие к изменениям другой связанной сущности.

  • Отсутствие согласованности между кодом и документацией — в документации свои термины, в коде свои.

  • В коде используются термины, которых нет в бизнес-документации.

  • Модели становятся слишком сложными и громоздкими.

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

  • Сложности в поддержке кода новыми членами команды.

  • Частые баги из-за неправильного понимания требований.

  • Проблемы с интеграционным тестированием из-за несоответствия определений терминов в коде и спецификациях продукта.

Как избежать проблем

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

  • Фиксируйте язык в документах, задачах, документации.

  • Регулярно проводите сессии уточнения и пересмотра терминологии.

  • Проверяйте соответствие терминологии в коде и документации.

  • Избегайте технических жаргонов.

Ubiquitous Language — это основа DDD. Если его нет, архитектура рушится, код становится неуправляемым, а команда тратит время на бесконечные уточнения. Язык должен постоянно развиваться вслед за бизнесом. Поэтому необходимо следить за признаками и вовремя корректировать язык.

Типичные ошибки при создании Bounded Context

Одним из ключевых элементов DDD является разделение системы на Bounded Contexts («ограниченные контексты»), которые позволяют представить сложную систему в виде отдельных логических автономных частей. Однако, неправильное формирование Bounded Context может привести к ряду серьезных ошибок, которые могут вызвать серьезные последствия: от усложнения разработки до полной деградации архитектуры. Рассмотрим наиболее распространенные проблемы и признаки неправильного формирования Bounded Context.

Недостаточное разделение контекста

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

Признаки:

  • Наличие множества разнородных сущностей и сервисов в одном контексте.

  • Частые конфликты именований между разными частями приложения.

  • Сложность внесения изменений из-за взаимосвязей внутри одного контекста.

Чрезмерная фрагментация

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

Признаки:

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

  • Множество интерфейсов для интеграции между контекстами.

  • Потеря целостности понимания работы всей системы.

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

Размытие границ контекста и смешивание контекстов

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

Признаки:

  • Несоответствие терминов и понятий между различными контекстами.

  • Конфликты в реализации функциональности, которая относится к разным контекстам.

  • Модели из других контекстов используются в текущем.

  • Нарушение принципов единой ответственности (SRP).

Отсутствие четких контрактов и API

Каждый контекст должен иметь хорошо определенный контракт взаимодействия с внешними по отношению к нему контекстами. Когда не формализованы и не определены способы взаимодействия между контекстами (Shared Kernel, Customer-Supplier, Anti-Corruption Layer и др.), возникают проблемы интеграции и совместимости версий.

Признаки:

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

  • Появляется сильная связанность между контекстами (high coupling).

  • Контексты хаотично зависят друг от друга.

  • Затруднения при обновлении версии API без нарушения работоспособности зависимых компонент.

Как избежать ошибок?

  • Четко определить границы: Границы контекста должны отражать реальные процессы и потребности бизнеса. Не формировать границы на основе технических слоев и компонентов.

  • Использовать Event Storming: Для определения границ стоит использовать практику Event Storming. В первую очередь он проводится для представителей бизнеса, а не для разработчиков.

  • Формализовать контракт: Необходимо создать чёткий интерфейс взаимодействия между контекстами и определить способы взаимодействия между контекстами.

  • Минимизация пересечения контекстов: Контекст должен охватывать ровно столько функций, сколько требуется для решения конкретной задачи. Модель может повторяться в разных контекстах, но с разной семантикой.

  • Постоянный мониторинг состояния: Регулярно проверять состояние контекста, следить за изменениями в требованиях и вовремя пересматривать границы контекста.

  • Рефакторить и менять границы контекста: Не стоит бояться рефакторить код и изменять границы контекстов. Бизнес-цели изменяются, процессы эволюционируют, и вслед за ними должны эволюционировать ограниченные контексты.

Успешное формирование Bounded Context обеспечивает успешную реализацию подходов Domain Driven Design и существенно упрощает разработку сложных информационных систем. Ошибки при формировании границ контекстов могут привести к множеству скрытых долгосрочных проблем, от увеличенного времени разработки до невозможности дальнейшего масштабирования.

Типичные ошибки в создании Aggregates и Entities

Domain Driven Design — мощный подход к проектированию сложных систем, который обещает порядок в сложных бизнес-процессах, однако он также скрывает ряд ловушек, связанных с проектированием агрегатов (Aggregates), сущностей (Entities). Без должного внимания к процессу легко допустить ошибки, способные привести к спагетти-коду на стероидах. Рассмотрим основные типы распространенных ошибок формирования агрегатов и сущностей.

Слишком большие агрегаты ("God Objects")

Часто встречается ситуация, когда один Aggregate содержит слишком много логики и объектов, это приводит к:

  • Повышенной сложности поддержки и внесения изменений в структуру системы.

  • Нарушению принципа единственной ответственности (Single Responsibility Principle, SRP).

  • Конфликтам при параллельных изменениях.

  • Падению производительности (тяжёлые операции чтения/записи).

Симптомы:

  • Агрегат редко загружается целиком, обычно нужны только его части.

  • Изменение даже небольшой части агрегата требует тщательного анализа всех последствий.

  • Частые deadlock’и или оптимистичные блокировки в БД.

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

Еще необходимо учитывать, что DDD очень часто поощряет создание сущностей, у которых нарушается принцип SRP, поэтому важно держать золотую середину и не сваливаться в крайности — анемичную модель или God Object.

Объединение независимых объектов в единый агрегат

В агрегат включаются сущности, фактически не зависимые друг от друга.

Симптомы:

  • В пределах одного агрегата отсутствуют явные и прямые отношения между отдельными объектами.

  • Сущности редко обновляются совместно или параллельно.

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

Неправильные границы агрегатов

Граница агрегата определяет единицу изменения данных. Когда граница определена некорректно, возникает риск потери контроля над целостностью данных. Это приводит к следующим последствиям:

  • Частичные обновления — система остается в несогласованном состоянии.

  • Сложные компенсирующие операции — приходится вручную откатывать изменения.

  • Увеличивается число запросов между агрегатами.

Симптомы:

  • Увеличивается взаимозависимость между различными агрегатами.

  • Сложность с обеспечением согласованности данных в рамках отдельных транзакций.

  • Много "внешних" проверок перед сохранением.

  • Часто используются JOIN между таблицами разных агрегатов в прикладном коде.

Отсутствие бизнес-логики при создании сущностей

Целью каждой сущности (Entity) является реализация определенного поведения, характерного ее предметной области. Но нередко встречаются случаи, когда бизнес-логику намеренно выносят за пределы самой сущности, делегируя её внешним сервисам. Это приводит к:

  • Существованию объектов в недопустимых вариациях и состояниях.

  • Дублированию проверок — валидация повторяется в разных слоях.

Симптомы:

  • Бизнес-логика вынесена наружу, не встроена непосредственно в сам объект.

  • Отсутствует очевидная связь между поведением объекта и внешними функциями.

  • Много сеттеров (setXXX()), а не методов с осмысленными именами (completeOrder()).

Как избежать перечисленных ошибок?не связанными терминами (кто они? термины могут называться терминами?)

Чтобы правильно спроектировать Aggregates и Entities, следует придерживаться нескольких важных принципов:

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

  • Применять Ubiquitous Language. Определить чёткий словарь терминов и использовать его всеми участниками команды для обеспечения единообразия в понимании ключевых понятий.

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

  • Снижать уровень зависимость. Минимизировать количество ссылок и отношений между разными частями приложения.

  • Соблюдать принцип единой ответственности (SRP). Каждая сущность должна отвечать строго за ограниченный круг задач.

DDD — это про то, как структурировать бизнес-логику так, чтобы она была надёжной, гибкой и понятной. И агрегаты, и сущности — ключевые строительные блоки этой архитектуры.

Заключение

Как я уже говорил выше, Domain Driven Design — это философия, она не дает четких шагов, выполнение которых позволит сделать отличный масштабируемый проект, полностью удовлетворяющий требованиям бизнеса. Вместо этого он дает фундаментальные принципы, направления движения, следование которым позволит вам сделать очень сложные системы простыми в понимании и поддержке. Этот подход предлагает вам начать с установления хорошего взаимопонимания между бизнесом и разработкой, а потом переносить четко сформулированные требования в код систем.

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

Спасибо за внимание!

Трансформируйтесь от хаоса к гармонии.

Если интересно, подписывайтесь на мой канал, там больше тем, которые не входят в формат Хабра.

Теги:
Хабы:
+5
Комментарии8

Публикации

Ближайшие события