Инженерия ПО
Domain-Driven Design
Цели урока
- Понимать, что DDD решает не код, а коммуникацию между бизнесом и инженерами
- Знать ключевые тактические паттерны: Entity, Value Object, Aggregate, Domain Event
- Видеть Bounded Context как границу одной модели данных, а не один сервис
- Использовать Ubiquitous Language в коде, тестах и обсуждениях с бизнесом
- Распознавать ситуации, где DDD оправдан (сложный домен), и где он избыточен (CRUD)
Предварительные знания
- Базовое ООП: классы, инкапсуляция, наследование
- Понимание SOLID и базовых архитектурных паттернов
- Опыт работы над приложением с непростой бизнес-логикой (банкинг, e-commerce, логистика)
2003 год: Эрик Эванс публикует 'Domain-Driven Design'. 560 страниц о том, как код должен отражать бизнес-реальность, а не наоборот. 20 лет спустя DDD стал языком архитектурных разговоров в Amazon, Google, Netflix. Bounded Context - это не просто концепция, это граница между командами. Conway's Law: структура системы отражает структуру организации. DDD помогает сделать эту структуру явной и осознанной.
- **Amazon**: каждый сервис соответствует Bounded Context - Orders, Payments, Inventory, Recommendations - со своей командой, моделью и language
- **Uber**: Driver контекст и Rider контекст имеют разные модели Trip - для водителя это маршрут и заработок, для пассажира это время и цена
- **Shopify**: модель Product в Storefront контексте (SEO, фото, описание) принципиально отличается от Product в Fulfillment контексте (SKU, вес, склад)
Bounded Context
Bounded Context (ограниченный контекст) - явная граница, внутри которой доменная модель последовательна и имеет единый язык (Ubiquitous Language). Эрик Эванс в книге DDD 2003 года: одна и та же концепция может означать разные вещи в разных контекстах. 'Customer' в контексте продаж - лид с воронкой сделок. 'Customer' в контексте поддержки - тикеты и SLA. 'Customer' в контексте биллинга - подписки и платежи. Попытка создать единую Customer модель для всех контекстов создаёт монстра.
Context Map показывает отношения между контекстами: Shared Kernel (два контекста разделяют часть модели), Customer-Supplier (upstream контекст влияет на downstream), Anti-Corruption Layer (ACL защищает от загрязнения внешней моделью). Microservices часто соответствуют Bounded Context один к одному, но это не требование - один контекст может быть реализован несколькими сервисами или наоборот.
E-commerce система имеет одну большую модель Product, используемую в каталоге, складе и биллинге. Какую проблему это создаёт по DDD?
Aggregates
Aggregate - кластер объектов (Entity и Value Objects), который обрабатывается как единица консистентности. Aggregate Root - единственная точка входа для изменений агрегата. Правило: внешние объекты могут хранить ссылку только на Aggregate Root, но не на его внутренние сущности. Это обеспечивает инварианты (бизнес-правила) всегда соблюдены внутри агрегата.
Размер агрегата влияет на производительность и конкурентность. Большой агрегат (Order с 1000 items) - одна транзакция блокирует всё, много конфликтов при параллельном доступе. Маленькие агрегаты - меньше конфликтов, быстрее. Правило Вернона: 'если сомневаешься - делай агрегат меньше'. Если два агрегата часто изменяются вместе - возможно они один агрегат. Если редко - возможно eventual consistency достаточно.
Внешний сервис хочет обновить цену OrderItem напрямую через его ID (без обращения к Order). Почему это нарушает DDD?
Entities и идентичность
Entity - объект, чья идентичность определяется не атрибутами, а уникальным ID. Два объекта Customer с одинаковым именем и email - разные Entity если у них разные ID. Атрибуты меняются (имя, адрес, email), но идентичность остаётся. В базе данных Entity соответствует строке с первичным ключом. Lifecycle: создание, изменение состояния, возможно удаление.
Repository pattern - абстракция для хранилища агрегатов. Repository принимает и возвращает полностью сформированные агрегаты, скрывая детали хранения. Принцип: один Repository на один Aggregate Root. OrderRepository сохраняет Order вместе с OrderItems - одной операцией, не по отдельности. Это обеспечивает атомарность транзакции по границам агрегата.
OrderRepository.save() получает Order агрегат и сохраняет его. Как должна работать транзакция?
Value Objects
Value Object - объект, чья идентичность определяется совокупностью атрибутов, не ID. Два Money(100, USD) объекта равны, потому что их атрибуты равны - нет смысла различать 'этот доллар' от 'того доллара'. Value Objects иммутабельны: вместо изменения создаётся новый объект. Это устраняет целый класс багов с неожиданными мутациями через shared references.
Примеры Value Objects: Money (amount + currency), Address (street, city, country), Email, PhoneNumber, DateRange, Coordinates. Всё что описывает 'что это такое', а не 'кто это' - кандидат в Value Object. Преимущества: можно передавать и возвращать без опасения мутации, equals() работает через структурное сравнение, содержат поведение связанное с данными (Money.add(), DateRange.overlaps()).
DDD применяется только для микросервисов - в монолите он избыточен
DDD применяется независимо от физической архитектуры. Bounded Contexts можно реализовать как модули в монолите (Modular Monolith) - это часто правильный первый шаг перед разбивкой на сервисы
Ценность DDD - в явных границах и Ubiquitous Language, а не в физическом разделении. Стартовать с Modular Monolith по DDD проще: нет сетевых вызовов, нет распределённых транзакций, те же концептуальные границы
Что отличает Entity от Value Object в DDD?
Ключевые идеи
- **Bounded Context**: явная граница с единым языком - одна концепция (Customer) означает разное в разных контекстах, и это нормально
- **Aggregate**: кластер объектов как единица консистентности, Aggregate Root - единственная точка изменения, транзакция = граница агрегата
- **Entity vs Value Object**: Entity идентифицируется по ID (кто), Value Object - по значению (что) и иммутабелен
Связанные темы
DDD определяет концептуальную архитектуру для многих паттернов:
- Event Sourcing и CQRS — Domain Events - механизм интеграции между Bounded Contexts в DDD; CQRS решает проблему разных моделей для чтения и записи
- Монолит vs Микросервисы — Bounded Context - единица разбивки: каждый микросервис должен соответствовать одному контексту или нескольким тесно связанным
Вопросы для размышления
- Как определить правильный размер Aggregate - слишком большой создаёт проблемы с конкурентностью, слишком маленький - сложность координации?
- Ubiquitous Language требует постоянного диалога между разработчиками и бизнесом. Как организовать этот процесс в реальной команде?
- Когда DDD overhead оправдан, а когда простая CRUD модель лучше?