Основы Domain-Driven Design от стратегического проектирования до тактических инструментов
Узнайте, как методология Domain-Driven Design помогает синхронизировать бизнес-логику с технической архитектурой системы. Разберем ключевые концепции стратегического и тактического проектирования для крупных Enterprise-проектов.
Введение
Domain-Driven Design (DDD) — это методология проектирования программного обеспечения, которая ставит во главу угла глубокое понимание бизнес-логики как основу архитектуры системы. В отличие от традиционных подходов, ориентированных преимущественно на структуру данных или технические интерфейсы, DDD фокусируется на моделировании предметной области. Это позволяет разработчикам говорить на одном языке с заказчиком и создавать сложные программные решения, которые максимально точно соответствуют реальным бизнес-процессам.
При разработке крупных Enterprise-проектов основной проблемой становится управление сложностью: системы быстро превращаются в запутанную сеть зависимостей, где изменение одной функции вызывает непредсказуемые последствия в других частях приложения. Чтобы избежать этого сценария, методология DDD предлагает четкое разделение дизайна на стратегический и тактический уровни. Стратегическое проектирование помогает определить границы ответственности и общие правила взаимодействия между модулями, а тактическое — предоставляет конкретные инструменты для реализации логики внутри каждого выделенного контекста.
В данной статье мы подробно разберем оба уровня проектирования. Вы узнаете, как определять границы контекстов и выстраивать общий язык (Ubiquitous Language) на стратегическом уровне, какие строительные блоки кода использовать в тактической части разработки, а также рассмотрим связующие механизмы — события и архитектурные слои, которые обеспечивают целостность и масштабируемость всей системы.
Стратегическое проектирование: определение границ и языка
На этапе стратегического проектирования в методологии Domain-Driven Design (DDD) основная задача — синхронизировать техническую архитектуру с бизнес-логикой предприятия. Это позволяет избежать ситуации, когда код говорит на одном языке, а пользователи системы — на другом.
Ubiquitous Language (Единый язык)
Первым шагом является создание Ubiquitous Language. Это общий словарь терминов, который используется всеми участниками процесса: от бизнес-аналитиков до SRE-инженеров. Цель — устранить семантические разрывы. Если в бизнесе «Заказ» (Order) считается завершенным только после оплаты, то и в коде статус ORDER_COMPLETED должен соответствовать именно этому событию, а не просто созданию записи в БД.
Bounded Contexts (Ограниченные контексты)
Чтобы единый язык не превратился в хаос из противоречивых определений, мы используем Bounded Contexts. Это границы ответственности, внутри которых конкретные термины имеют строго фиксированное значение. Например, объект «Товар» может иметь разные модели:
- В контексте Продаж: цена, описание, наличие на складе.
- В контексте Логистики: вес, габариты, требования к температурному режиму.
Context Mapping и архитектурные связи
Для управления взаимодействием между контекстами используется Context Mapping. Он визуализирует потоки данных и определяет стратегии интеграции:
- Shared Kernel: общее использование кода или базы данных (использовать с осторожностью).
- Customer/Supplier: один контекст предоставляет данные для другого по четкому контракту.
- Anti-Corruption Layer (ACL): слой адаптации, защищающий новый домен от «загрязнения» устаревшей логикой или специфическими форматами внешних API.
Пример реализации ACL для изоляции специфики внешней системы:
# Внешний API возвращает данные в специфическом формате
class ExternalLegacyProduct:
def get_raw_data(self):
return {"item_id": "123", "cost_amt": 50.0, "is_active_flag": True}
# Anti-Corruption Layer преобразует их в наш чистый внутренний домен
class ProductAdapter:
def __init__(self, legacy_service: ExternalLegacyProduct):
self.legacy_service = legacy_service
def get_product(self) -> DomainProduct:
raw = self.legacy_service.get_raw_data()
# Мы мапим данные в структуру нашего домена, скрывая сложность внешнего API
return DomainProduct(id=raw["item_id"], price=raw["cost_amt"])
Разделение доменов и масштабируемость
Наконец, стратегическое проектирование подразумевает декомпозицию крупных бизнес-доменов на поддомены. Это критически важно для обеспечения независимости команд разработки: каждая команда владеет своим контекстом, может независимо разворачивать микросервисы и масштабировать ресурсы без влияния на соседние области системы.
Тактическое проектирование: строительные блоки кода
Если стратегическое проективание определяет границы контекстов и общий язык, то тактическое — это перевод этих абстракций в конкретные программные структуры. Основная задача здесь заключается в создании модели, которая максимально точно отражает бизнес-логику, минимизируя побочные эффекты.
Entities и Value Objects
Первичным строительным блоком является разграничение объектов на Entities (Сущности) и Value Objects (Объекты-значения):
- Entities обладают уникальной идентичностью, которая остается неизменной в течение жизненного цикла объекта. Пример: пользователь или заказ. Состояние сущности может меняться, но её "личность" — нет.
- Value Objects определяются исключительно своими атрибутами и являются неизменяемыми (immutable). Если два объекта-значения имеют одинаковые данные, они считаются эквивалентными. Пример: денежная сумма, адрес или цвет.
// Value Object (Immutable)
class Money {
constructor(public readonly amount: decimal, public readonly currency: string) {}
}
// Entity (Identity + Mutable State)
class BankAccount {
private id: string; // Уникальный идентификатор
private balance: Money;
deposit(amount: Money) {
this.balance = this.balance.add(amount); // Изменение состояния при сохранении ID
}
}Aggregates (Агрегаты)
Для обеспечения целостности данных в сложных системах используются Агрегаты — кластеры связанных объектов, которые рассматриваются как единая единица изменения. Агрегат имеет "корень" (Aggregate Root), через который осуществляется любой доступ к внутренним сущностям. Это гарантирует соблюдение бизнес-инвариантов внутри одной транзакции.
Domain Services
Иногда бизнес-логика не может быть естественным образом инкапсулирована ни в одну сущность, ни в агрегат (например, сложный расчет налогов или процесс перевода между разными счетами). В таких случаях применяются Domain Services — объекты, предназначенные исключительно для выполнения действий над несколькими домеinn-объектами.
Repositories и Factories
Для работы с инфраструктурой используются паттерны абстракции:
- Repositories предоставляют интерфейс к коллекции агрегатов, скрывая детали реализации хранения (SQL, NoSQL или In-memory).
- Factories отвечают за создание сложных объектов, требующих сложной логики инициализации перед тем, как они попадут в граф зависимостей.
Связующие механизмы: события и архитектурные слои
Переход от тактических строительных блоков к работающей системе требует четких правил взаимодействия между компонентами. Основная задача здесь — обеспечить связность системы, сохраняя при этом высокую степень декомпозиции и независимость модулей.
Domain Events: реактивность через факты
Доменные события (Domain Events) являются ключевым механизмом уведомления других контекстов об изменениях в состоянии системы. В отличие от команд, которые выражают намерение совершить действие, события — это свершившиеся факты прошлого.Bounded Context (например, «Заказ оплачен»), другие контексты могут реагировать на него асинхронно, не создавая жестких связей между модулями.
Application Services vs Domain Services
Для чистоты архитектуры критически важно разделять ответственность за оркестрацию и бизнес-логику:
- Application Services: отвечают за координацию процесса. Они принимают входные данные, управляют транзакциями, взаимодействуют с репозиториями и отправляют уведомления.
- Domain Services: содержат чистую бизнес-логику, которая не помещается в конкретную сущность (Entity) или объект значения (Value Object).
// Application Service (Оркестрация)
class OrderApplicationService {
async placeOrder(dto: PlaceOrderDto) {
const order = Order.create(dto.items); // Создание сущности
await this.repository.save(order); // Взаимодействие с инфраструктурой
this.eventBus.publish(new OrderPlaced(order.id)); // Реактивность
}
}
// Domain Service (Чистая логика)
class DiscountCalculator {
calculateSpecialPrice(items: Item[]): Money {
// Сложная математика скидок, не относящаяся к одной сущности
return items.reduce(...);
}
}