Типы классов в DDD
Модель и масштаб — Основы БД, опорные темы, проектирование БД, пакетная работа. Карта — о разделе.
Предметная область — это сфера деятельности или бизнеса, в рамках которой создается программное обеспечение. Это та реальная область знаний, которую программа должна отражать и автоматизировать. В предметную область входят все процессы, правила, сущности и взаимосвязи, которые существуют в реальном мире. Например, для интернет-магазина предметной областью будет розничная торговля с ее процессами заказов, оплат, доставок, управления складом и клиентами. Для банка — это финансовая сфера с кредитами, депозитами, транзакциями, клиентскими счетами и всеми юридическими требованиями. Главная задача разработки — создать такую программную модель, которая максимально точно отражала бы предметную область, позволяя бизнесу эффективно работать через цифровые инструменты.
Domain-Driven Design (DDD), или предметно-ориентированное проектирование — это подход к разработке программного обеспечения, который ставит в центр внимания бизнес-логику и правила реальной предметной области (домена), а не технические детали реализации, базы данных или фреймворки. Этот метод был сформулирован разработчиком Эриком Эвансом в 2003 году с целью помочь программистам и представителям бизнеса говорить на одном языке и создавать сложные ИТ-системы без потери важного контекста.
В доменной модели описаны смысл и правила предметной области. В классификации типов классов в ООП — общие архетипы (Controller, Repository, DTO). DDD (Domain-Driven Design) задаёт отдельный набор типов классов доменного слоя: объекты с идентичностью, объекты-значения, агрегаты, сервисы и события. Их имена и границы должны совпадать с общим языком (Ubiquitous Language), которым говорят разработчики и эксперты предметной области.
Детали реализации — это технические аспекты построения системы, которые не имеют прямого отношения к бизнес-логике и предметной области. К ним относятся выбор конкретной базы данных, фреймворков, способов хранения данных, протоколов обмена, архитектуры развертывания и множество других технических решений. В контексте DDD детали реализации считаются вторичными — их следует скрывать от бизнес-логики, чтобы последняя могла развиваться независимо. Например, решение использовать PostgreSQL или MongoDB, REST или gRPC, способ маппинга объектов в таблицы базы данных, использование кэша, настройки пула соединений, формат логов — всё это детали реализации. Они должны быть вынесены в инфраструктурный слой, чтобы при смене технологии ядро системы, содержащее бизнес-логику, оставалось неизменным и не требовало переписывания.
После Доменной модели и до углубления в Паттерны проектирования (таблица DDD-паттернов).
Для стратегического DDD (bounded context, контекстные карты) см. ту же главу про домен и раздел про декомпозицию в Стратегии декомпозиции монолитных систем.
Bounded Context — это четко очерченная логическая граница внутри системы, в рамках которой определенные термины, сущности и бизнес-правила имеют строгое и однозначное значение. Это понятие решает проблему неоднозначности, когда одно и то же слово может означать разные вещи в разных частях системы. Например, в контексте авторизации «пользователь» — это логин, пароль, роли и права доступа, а в контексте доставки «пользователь» — это имя, фамилия, адрес и телефон. Попытка создать единую модель «пользователя» для всей системы приводит к раздутым классам с десятками полей и путанице в логике. Bounded Context позволяет изолировать эти модели друг от друга, создавая четкие границы. В идеале каждый ограниченный контекст соответствует одному бизнес-капабилити или поддомену, а команды разработчиков могут работать над разными контекстами независимо, используя свои собственные модели и языки.
Контекстная карта — это стратегический артефакт DDD, представляющий собой схему или документ, который показывает все ограниченные контексты в системе и способы их взаимодействия друг с другом. Она описывает, какие интеграционные паттерны используются между контекстами, например, партнерство, общее ядро, антикоррупционный слой, клиент-поставщик или публикация событий. Контекстная карта помогает командам понять общую архитектуру системы, увидеть, как данные и команды передаются между различными частями, и выявить потенциальные проблемы интеграции. Например, на карте может быть показано, что контекст «Заказы» передает события о создании заказа в контекст «Доставка», а контекст «Доставка» отправляет команды на обновление статуса в контекст «Уведомления». Контекстная карта также помогает в планировании развития системы, показывая, какие контексты независимы, а какие сильно связаны и требуют совместного изменения.
Два слоя — домен и приложение
Уровень или слой подхода DDD включает две основные группы. Стратегический уровень занимается организацией системы в целом и определяет, как разделить сложную предметную область на управляемые части. Здесь работают с доменом, поддоменами, ограниченными контекстами, контекстной картой и единым языком. Этот уровень отвечает на вопросы о границах системы, о том, какую часть бизнеса автоматизирует тот или иной модуль, и как модули взаимодействуют. Тактический уровень работает внутри одного ограниченного контекста и определяет конкретные строительные блоки для реализации бизнес-логики на уровне кода. Здесь используются сущности, объекты-значения, агрегаты, репозитории, фабрики, доменные сервисы и события. Тактическое проектирование отвечает на вопросы о том, как именно реализовать правила предметной области, как организовать классы и методы, чтобы код был понятным, надежным и легко изменяемым.
Путаница чаще всего возникает между доменным и прикладным (application) слоями.
| Слой | Кто координирует | Примеры классов |
|---|---|---|
| Домен | Правила предметной области | Order, Money, OrderPlaced, TransferService (доменный) |
| Прикладной | Сценарии использования, транзакции, вызов инфраструктуры | PlaceOrderHandler, PlaceOrderCommand, оркестрация репозиториев |
| Инфраструктура | БД, HTTP, очереди | OrderRepository (реализация), SmtpEmailSender |
| Представление | Ввод-вывод | OrdersController, OrderDto |
Доменный сервис выражает операцию на языке бизнеса (TransferMoney, CalculateShipping). Сервис приложения (use case) загружает агрегаты, вызывает домен, сохраняет изменения и публикует интеграционные события — без дублирования бизнес-правил.
Правила предметной области — это бизнес-требования, инварианты и ограничения, которые определяют, как должна работать система в соответствии с реальными процессами компании. Эти правила являются сердцем DDD и должны быть явно выражены в коде, а не спрятаны в комментариях или документации. Примеры правил: клиент не может заказать товар, если его счет заблокирован; сумма кредита не может превышать определенный процент от дохода; заказ нельзя отменить после того, как он был отправлен в доставку; при добавлении товара в корзину общая сумма должна пересчитываться автоматически; пользователь должен подтвердить email в течение 24 часов после регистрации. Правила предметной области часто меняются вместе с бизнесом, поэтому важно, чтобы они были локализованы в одном месте (обычно внутри агрегатов или доменных сервисов) и легко обновлялись без затрагивания технических слоев.
Сценарии использования — это конкретные взаимодействия пользователя или внешней системы с вашей системой, которые приводят к какому-то результату и изменяют состояние системы. Они описывают, что именно должна делать система в ответ на внешний запрос, и являются мостом между бизнес-требованиями и технической реализацией. В терминах DDD сценарии использования реализуются через Application Services, которые координируют работу доменных объектов. Примеры сценариев: клиент оформляет заказ; администратор блокирует пользователя; менеджер изменяет цену товара; система автоматически обновляет статус заказа при получении события от платежной системы. Каждый сценарий использования должен быть четко задокументирован, иметь входные данные, бизнес-правила, которые проверяются, и ожидаемый результат, включая возможные ошибки и исключительные ситуации.
Агрегаты — это группа связанных объектов, которые рассматриваются как единое целое для изменения и обеспечения целостности данных. Агрегат представляет собой кластер сущностей и объектов-значений, внутри которого все изменения должны быть согласованы. У каждого агрегата есть корневая сущность, называемая корнем агрегата, через которую осуществляются все операции. Эта структура гарантирует, что бизнес-правила, действующие внутри группы, никогда не нарушаются. Например, агрегатом является заказ со всеми его позициями. Вы не можете изменить позицию отдельно от заказа, потому что нужно пересчитать итоговую сумму, проверить статус заказа и соблюсти инварианты. Агрегат предоставляет транзакционную границу — если вам нужно сохранить изменения, весь агрегат сохраняется целиком. Внешние объекты могут ссылаться на агрегат только через его корень и только по идентификатору, не имея доступа к внутренним деталям.
Карта типов классов в DDD
В DDD существует несколько четко определенных типов классов, у каждого своя зона ответственности.
┌─────────────────────────────────────────────────────────────────┐
│ ВНЕШНИЙ МИР │
│ (UI, API, консоль, другие сервисы) │
└────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ПРЕДСТАВЛЕНИЕ (Presentation) │
├─────────────────────────────────────────────────────────────────┤
│ ● Controller / UI-адаптер — принимает запросы │
│ ● DTO (Data Transfer Object)— "плоские" объекты для передачи │
│ ● View Model — данные для отображения │
└────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ПРИЛОЖЕНИЕ (Application) │
├─────────────────────────────────────────────────────────────────┤
│ ● Use Case / Service — команды/запросы системы │
│ ● Command / Query — инкапсулированные запросы │
│ ● DTO (иногда) — передача между слоями │
└────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ДОМЕН (Domain) ★ │
├─────────────────────────────────────────────────────────────────┤
│ ● Агрегат (Aggregate) — корень с бизнес-логикой │
│ ● Сущность (Entity) — объект с ID │
│ ● Объект-значение (VO) — объект без ID │
│ ● Доменный сервис — логика, не влезающая в объект │
│ ● Доменное событие — факт, который произошёл │
│ ● Репозиторий (интерфейс) — абстракция хранения │
│ ● Фабрика — создание сложных объектов │
│ ● Спецификация — бизнес-условие/фильтр │
└────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ИНФРАСТРУКТУРА (Infrastructure) │
├─────────────────────────────────────────────────────────────────┤
│ ● Репозиторий (реализация) — работа с БД, кэшем │
│ ● Реализация доменных сервисов — отправка email, API │
│ ● Message Bus / Event Bus — отправка событий │
│ ● ORM / маппинг — преобразование в БД │
└─────────────────────────────────────────────────────────────────┘
| Тип | Идентичность | Изменяемость | Типичное имя в коде | Слой |
|---|---|---|---|---|
| Entity (сущность) | Есть стабильный ID | Обычно изменяемая | Customer, Order | Домен |
| Value Object (объект-значение) | По полям | Неизменяемый | Money, Address, Email | Домен |
| Aggregate / Aggregate Root | Корень с ID | Граница согласованности | Order (корень), OrderLine (внутри) | Домен |
| Domain Service | Нет своего состояния | — | PricingService, FraudCheck | Домен (интерфейс) |
| Domain Event | Событие с меткой времени | Неизменяемый факт | OrderPlaced, PaymentFailed | Домен |
| Repository | — | — | IOrderRepository | Домен (контракт), инфраструктура (реализация) |
| Factory | — | — | OrderFactory | Домен |
| Specification | — | Правило как объект | ActivePremiumCustomerSpec | Домен |
Ниже — каждый тип подробнее.
Entity (сущность)
Entity или сущность — это объект предметной области, который обладает уникальной идентичностью, сохраняющейся на протяжении всего его жизненного цикла. Идентичность сущности определяется не набором ее атрибутов, а специальным идентификатором, который остается неизменным, даже когда все остальные данные меняются. Например, пользователь с идентификатором 123 остается тем же пользователем после смены имени, email и пароля. Сущности содержат бизнес-логику, которая управляет их состоянием, и защищают инварианты через методы, изменяющие состояние с проверками. Сущности часто являются частью агрегатов, но также могут существовать и как отдельные объекты. В коде сущность имеет поле ID, методы типа confirmEmail или blockUser, а равенство двух экземпляров определяется исключительно по идентификатору, а не по значениям полей.
Сущность — объект, который определяется устойчивым идентификатором, а не текущим набором полей. Пользователь с id = 42 остаётся тем же пользователем после смены email.
Признаки в коде:
- поле
Id(или value objectCustomerId); - методы, меняющие состояние с проверкой инвариантов (
ConfirmEmail(),Block()); - равенство по ID: два экземпляра с одним ID считаются одной сущностью.
Код ITЗагрузка примера кода…
Сущность может быть корнем агрегата или внутренним элементом агрегата (см. ниже).
Value Object (объект-значение)
Value Object или объект-значение — это объект, который не имеет собственной идентичности и полностью определяется набором своих атрибутов. Два объекта-значения с одинаковыми атрибутами считаются одинаковыми и взаимозаменяемыми. Объекты-значения создаются один раз и никогда не изменяются, то есть являются иммутабельными, что упрощает их использование и предотвращает ошибки. Если вам нужно изменить объект-значение, вы создаете новый экземпляр с новыми данными вместо изменения существующего. Примерами объектов-значений служат адрес, денежная сумма, диапазон дат, телефонный номер, координаты, цвет. В коде они имеют все поля помеченными как final, конструктор с валидацией, методы для создания новых экземпляров при необходимости изменений, правильно реализованные equals и hashCode по всем полям, и не содержат бизнес-методов, меняющих состояние.
Объект-значение описывает характеристику без собственной жизни в системе — деньги, адрес, диапазон дат, координаты. Два значения с одинаковыми полями взаимозаменяемы.
Практика:
- неизменяемость (
record,readonly struct, финальные поля); - валидация в конструкторе (
Amount > 0, формат email); - равенство по всем значимым полям;
- отсутствие суррогатного ID в БД (часто встраивается в таблицу сущности как колонки).
Код ITЗагрузка примера кода…
Значения удобно передавать между слоями внутри домена; наружу (в API) чаще отдают примитивы или DTO, собранные из value objects.
Aggregate и Aggregate Root (агрегат и корень)
Aggregate и Aggregate Root — это взаимосвязанные понятия, где агрегат представляет собой группу объектов, а корень агрегата является главной сущностью, через которую осуществляется доступ ко всей группе. Агрегат включает в себя корень и набор дочерних сущностей и объектов-значений, которые не могут существовать вне агрегата. Корень агрегата является единственной точкой входа для всех операций с агрегатом и защищает его внутреннюю целостность. Внешний мир может ссылаться на корень агрегата по его идентификатору, но не может напрямую манипулировать внутренними объектами. Например, корнем агрегата заказа является сам заказ, который содержит позиции заказа как дочерние сущности. Через заказ выполняются операции добавления, удаления или изменения позиций, при этом автоматически пересчитывается итоговая сумма и проверяются бизнес-правила. При сохранении в базу данных весь агрегат сохраняется как единое целое.
Агрегат — кластер сущностей и значений, который изменяется как единое целое в одной транзакции. Корень агрегата (Aggregate Root) — единственная сущность, на которую ссылаются извне.
Правила:
- внешний код вызывает только методы корня (
order.AddLine(...), а неorder.Lines[0].Quantity = 5); - инварианты проверяются внутри границы агрегата;
- репозиторий загружает и сохраняет агрегат целиком, а не отдельные строки без корня;
- ссылки между агрегатами — по ID, а не по прямым объектным ссылкам (избегают "большого комка грязи").
Пример границы: Order (корень) + коллекция OrderLine + возможно DeliveryAddress как value object. Отдельный агрегат Product на складе — другая граница; заказ хранит ProductId, а не живой объект Product.
Размер агрегата держат небольшим: один бизнес-инвариант на транзакцию. Слишком крупный агрегат ведёт к блокировкам и конфликтам при конкурентной записи.
Domain Service (доменный сервис)
Domain Service или доменный сервис — это класс, который содержит бизнес-логику, не укладывающуюся в рамки одной сущности или агрегата. Доменный сервис используется, когда операция требует взаимодействия нескольких агрегатов или когда логика кажется чужеродной для конкретной сущности. Доменный сервис должен называться на языке бизнеса и решать конкретную бизнес-задачу, а его методы должны быть частью единого языка предметной области. Важно, что доменный сервис оперирует только доменными объектами и не содержит технической логики, такой как работа с базами данных или вызовы внешних API. Например, сервис проверки кредитоспособности клиента может использовать информацию из нескольких агрегатов — счетов, кредитов и клиентских данных, — чтобы принять решение. Сервис расчета цены заказа может учитывать скидки клиента, акционные предложения и стоимость доставки, применяя сложные бизнес-правила.
Доменный сервис — операция предметной области, которую неудобно отнести к одной сущности:
- затрагивает несколько агрегатов (
TransferBetweenAccounts); - зависит от внешнего правила, но формулируется на языке домена (
ShippingCostCalculatorс тарифами как входными данными); - чистая политика без состояния (
UniqueUsernamePolicy— интерфейс в домене, реализация с запросом к БД в инфраструктуре).
Доменный сервис не знает про HTTP, EF Core, Kafka. Он принимает и возвращает доменные типы.
Domain Event (доменное событие)
Domain Event или доменное событие — это значимое событие в предметной области, которое произошло в системе и может быть интересно другим частям системы. Событие фиксирует факт, что что-то случилось, и содержит всю необходимую информацию об этом факте, обычно в прошедшем времени. Примеры событий: заказ оплачен, пользователь зарегистрирован, товар закончился на складе, доставка отправлена. Генерация событий обычно происходит внутри агрегатов при изменении их состояния. После сохранения агрегата в репозитории события публикуются в шину сообщений и обрабатываются соответствующими обработчиками, которые могут выполнять дополнительные действия, например, отправлять уведомления, обновлять аналитику или синхронизировать другие контексты. Использование доменных событий позволяет строить слабосвязанные системы, разделять обработку действий и реализовывать паттерны типа eventual consistency.
Доменное событие фиксирует факт, который уже произошёл в предметной области: OrderPlaced, InvoiceIssued. Имя — в прошедшем времени. Событие неизменяемо и содержит данные, нужные подписчикам (ID агрегата, сумма, метка времени).
Типичный поток:
- метод корня агрегата меняет состояние и добавляет событие в коллекцию
DomainEvents; - слой приложения после успешного
Saveпубликует события в шину или вызывает обработчики; - побочные эффекты (email, аналитика) живут в обработчиках, а не внутри сущности.
События помогают развязать подсистемы и строить event sourcing там, где нужен полный аудит изменений.
Repository (репозиторий)
Repository или репозиторий — это механизм, предоставляющий абстракцию для хранения и извлечения агрегатов, скрывающий детали работы с конкретной базой данных. Интерфейс репозитория принадлежит доменному слою и определяет методы для работы с агрегатами, такие как сохранение, поиск по идентификатору, поиск по критериям и удаление. Реализация репозитория находится в инфраструктурном слое и содержит код взаимодействия с выбранной базой данных, будь то реляционная СУБД, NoSQL, файловая система или внешний сервис. Репозиторий работает на уровне агрегатов, а не отдельных таблиц, и возвращает полностью собранные объекты агрегатов. Например, репозиторий заказов имеет метод findById, который загружает заказ со всеми его позициями, адресами и связанными данными в единый объект, готовый к использованию доменной логикой.
Репозиторий в DDD — абстракция коллекции агрегатов в доменном слое — GetById, Add, иногда Find по спецификации. Методы называют по-русски/английски в терминах домена: FindOpenOrdersByCustomer, а не GetAllFromTableOrders.
- интерфейс
IOrderRepositoryобъявляют в домене; - реализацию с ORM, SQL, кэшем помещают в инфраструктуру;
- репозиторий возвращает полный агрегат, готовый к вызову доменных методов.
Репозиторий не содержит бизнес-правил ("можно ли отменить заказ" — в Order.Cancel()).
Factory (фабрика)
Factory или фабрика — это конструктивный паттерн, предназначенный для инкапсуляции сложной логики создания агрегатов или других доменных объектов. Фабрика используется, когда создание объекта требует множества шагов, проверок, вычислений или использования дополнительных сервисов, и помещать эту логику в конструктор было бы неправильно. Фабрика может быть как отдельным классом, так и статическим методом на самом агрегате, если логика создания не слишком сложная. Она обеспечивает создание объектов в корректном состоянии, соблюдая все бизнес-правила. Например, фабрика заказов может принимать корзину покупателя, проверять наличие товаров на складе, рассчитывать скидки, создавать сам заказ и его позиции, возвращая готовый к сохранению агрегат. Фабрика может также создавать объекты-значения, требующие сложной валидации.
Фабрика инкапсулирует сложное создание агрегата, когда конструктор раздувается или нужны проверки на нескольких источниках:
OrderFactory.CreateFromCart(cart, customerId);- сборка агрегата из нескольких value objects с согласованностью.
Простые сущности создают через конструктор или статический метод Create на самой сущности; фабрика — для нетривиальных правил инициализации.
Specification (спецификация)
Specification или спецификация — это паттерн, который инкапсулирует бизнес-правило или условие в виде отдельного объекта, позволяя переиспользовать его для проверки объектов и построения запросов к хранилищу. Спецификация содержит метод, проверяющий, удовлетворяет ли некоторый объект указанному условию. Это позволяет выделить бизнес-правила из кода и использовать их в разных местах системы. Например, спецификация активного клиента проверяет, что клиент не заблокирован и имеет подтвержденный email. Спецификация высокого заказа определяет, что сумма заказа превышает определенный порог. В более продвинутых реализациях спецификации могут также использоваться для построения SQL-запросов или фильтрации данных, что позволяет применять одни и те же бизнес-правила как в коде, так и на уровне базы данных.
Спецификация выносит правило отбора или допустимости в отдельный объект: IsEligibleForDiscount, комбинируется через And / Or / Not. Удобно для единообразных фильтров в домене и для повторного использования в тестах.
В проектах с EF Core спецификацию иногда мапят на IQueryable; важно, чтобы смысл правила оставался в домене, а детали SQL — в инфраструктуре.
Что в DDD обычно не является доменным типом
| Класс | Роль | Почему не домен |
|---|---|---|
| DTO | Передача по сети | Нет поведения и инвариантов |
| Controller / Endpoint | HTTP, валидация формата | Слой представления |
| ViewModel | Экран | UI |
DbContext / Entity Framework сущность с атрибутами [Table] | Персистентность | Техническая модель (допустима отдельно от домена) |
| Application Service / Handler | Сценарий | Оркестрация, транзакции |
| Integration Event | Контракт между сервисами | Может отличаться от доменного события по полям и версии |
Допустим маппинг между доменной моделью и persistence-моделью (Order ↔ OrderEntity) — это разделение слоёв, а не дублирование логики.
DTO или Data Transfer Object — это простой объект, предназначенный исключительно для передачи данных между различными слоями приложения или между системами через сеть. DTO не содержит бизнес-логики, методов валидации или поведения — это просто контейнер с полями, геттерами и сеттерами, часто сериализуемый в JSON или XML. DTO используется для минимизации количества вызовов между слоями и для отделения внутренней модели предметной области от внешнего представления. Например, для API заказа создается специальный DTO, который содержит только те поля, которые могут понадобиться клиенту, возможно, в другом формате или с агрегированными данными. DTO также защищает доменные объекты от нежелательных изменений, так как слой представления работает только с DTO, а не с сущностями, что предотвращает случайное изменение бизнес-логики из внешнего кода.
Controller или контроллер — это компонент слоя представления, который отвечает за прием входящих запросов от клиентов, преобразование их параметров и вызов соответствующих сервисов приложения. Контроллер является точкой входа в систему для внешних запросов, будь то HTTP-запросы от веб-клиентов, сообщения от очередей или команды от других систем. В своей работе контроллер получает входящие данные в виде DTO, валидирует их на уровне формата, маппит в команды или запросы для Application Service, вызывает нужный Use Case и возвращает ответ клиенту. Контроллер не должен содержать бизнес-логики — его задача только в маршрутизации, преобразовании форматов и обработке ошибок представления. Он также может отвечать за аутентификацию и авторизацию пользователей на уровне доступа к эндпоинтам.
Endpoint или конечная точка — это конкретный URL, путь или канал, по которому клиент может обратиться к системе для выполнения определенного действия. Каждый эндпоинт соответствует одному сценарию использования или команде, которую система может выполнить. Эндпоинт определяет метод HTTP, параметры запроса, формат тела запроса и структуру ответа. В хорошо спроектированной системе эндпоинты имеют четкие, предсказуемые названия, соответствующие REST-принципам или другим принятым стандартам, и документируются через OpenAPI или аналогичные спецификации. Например, POST /orders создает новый заказ, GET /orders/{id} возвращает информацию о конкретном заказе, а PUT /orders/{id}/status изменяет статус заказа. Каждый эндпоинт должен быть защищен соответствующими механизмами аутентификации и авторизации.
ViewModel — это объект, специально созданный для передачи данных от контроллера к представлению в пользовательском интерфейсе. В отличие от DTO, ViewModel часто содержит не только данные, но и информацию о состоянии экрана, а также может включать в себя элементы управления, флаги для показа сообщений и вспомогательные вычисления, специфичные для отображения. ViewModel проектируется так, чтобы максимально упростить работу представления, часто объединяя данные из различных источников в одну удобную структуру. Важно, что ViewModel не содержит бизнес-логики, а только логику отображения. Например, ViewModel страницы профиля пользователя может содержать не только имя и email, но и флаги «показывать ли кнопку смены пароля» или «отображать ли предупреждение о незавершенном действии».
DbContext — это класс, используемый в ORM-фреймворках, особенно в Entity Framework, для представления сессии работы с базой данных, включающей в себя наборы сущностей, конфигурации маппинга и управление транзакциями. DbContext предоставляет интерфейс для запросов, отслеживания изменений, сохранения объектов и управления соединениями с базой данных. В DDD-приложениях DbContext обычно принадлежит инфраструктурному слою и используется в реализациях репозиториев для работы с persistence-моделью. Важно отметить, что DbContext работает с persistence-моделью, которая может отличаться от доменной модели, а не с самими доменными сущностями, и задача репозитория — преобразовывать данные между этими двумя моделями при сохранении и загрузке.
Application Service или сервис приложения — это класс, реализующий конкретный сценарий использования системы, который координирует работу доменных объектов, репозиториев и других инфраструктурных компонентов для достижения бизнес-цели. Application Service не содержит бизнес-логику — его задача организовать процесс: получить входные данные, найти нужные агрегаты, вызвать их методы, выполнить проверки через доменные сервисы, сохранить изменения и опубликовать события. Каждый Application Service обычно реализует один четкий сценарий использования, принимает одну команду и возвращает один результат. Он также обрабатывает транзакционность и обеспечивает консистентность операций. Например, сервис PlaceOrderUseCase принимает команду на создание заказа, загружает клиента, проверяет его статус, создает заказ через фабрику, сохраняет его и публикует событие о создании заказа.
Handler или обработчик — это компонент, который обрабатывает конкретный тип сообщения, команды или события в системе. В контексте DDD обычно говорят о двух видах обработчиков: Command Handlers, которые обрабатывают команды в CQRS-архитектуре и по сути являются Application Services, и Event Handlers, которые реагируют на доменные события или интеграционные события, выполняя побочные действия. Обработчик обычно является легковесным классом с единственной ответственностью — принять сообщение и выполнить соответствующую логику. Например, обработчик события OrderPaidEvent может отправить письмо клиенту, обновить статус в CRM и добавить запись в систему аналитики. Важно, что обработчики не должны содержать сложной бизнес-логики — они либо делегируют ее доменным сервисам, либо выполняют простые действия без принятия сложных решений.
Integration Event или интеграционное событие — это событие, которое публикуется для обмена информацией между различными ограниченными контекстами или даже между разными системами. В отличие от доменного события, которое существует внутри одного контекста, интеграционное событие предназначено для взаимодействия между контекстами и обычно содержит минимальный набор данных, необходимый для информирования других частей системы о случившемся факте. Интеграционное событие публикуется через механизмы межсервисного взаимодействия, такие как шины сообщений или очереди. Например, когда в контексте заказов создается новый заказ, публикуется интеграционное событие OrderCreated, которое может быть получено контекстом доставки для планирования отправки или контекстом уведомлений для отправки подтверждения клиенту.
Persistence-модель — это модель данных, специально спроектированная для хранения в базе данных и оптимизированная под способ хранения и запросы к хранилищу. В правильно спроектированном DDD-приложении persistence-модель отличается от доменной модели, потому что доменная модель оптимизирована для бизнес-логики, а persistence-модель — для хранения и извлечения данных. Например, доменный заказ может иметь сложную иерархическую структуру, а в базе данных он может быть представлен несколькими таблицами с внешними ключами или, наоборот, одной денормализованной таблицей. Задача маппинга между доменной и persistence-моделью обычно решается в репозиториях. Такой подход позволяет изменять способ хранения данных без влияния на бизнес-логику и наоборот.
Мини-практикум по классификации классов
Ниже короткие учебные вопросы для самостоятельной проверки:
Priceс полямиAmountиCurrencyбез ID — это Entity или Value Object?Orderс коллекциейOrderLine, где внешние вызовы идут только черезOrder— это агрегат или просто сущность?CalculateDeliveryCostиспользует адрес, вес и тарифы, но не хранит состояние — это Domain Service или Application Service?OrderPlacedсOrderIdиOccurredAt— это Domain Event или DTO?
Рекомендуемый порядок ответа:
- сначала определить границу ответственности;
- затем идентичность и жизненный цикл;
- затем место класса в слоях архитектуры.
Если классификация даётся с трудом, возвращайтесь к Доменной модели и Построению систем на основе классов и объектов.
Связь с "анемичной моделью"
Анемичная модель — это антипаттерн проектирования, при котором классы предметной области содержат только свойства с геттерами и сеттерами, но не содержат никакой бизнес-логики. В анемичной модели вся логика вынесена в отдельные сервисы, которые работают с этими классами как с простыми структурами данных. Этот подход нарушает основные принципы объектно-ориентированного проектирования и DDD, так как бизнес-правила оказываются размазанными по множеству сервисов, что приводит к дублированию кода, нарушению инкапсуляции и усложнению поддержки. Например, если у вас есть класс Order с методами addItem, removeItem и pay, содержащими проверки, это не анемичная модель. А если у Order есть только поля и сеттеры, а логика добавления товаров и оплаты находится в OrderService, это анемичная модель. В DDD следует избегать анемичных моделей, помещая бизнес-логику внутрь сущностей и агрегатов, где ей и место.
Анемичная модель — классы только с get/set и вся логика в "сервисах". Для DDD это потеря главного преимущества — инварианты размазаны, состояние можно испортить, минуя домен.
Здоровая модель: поведение рядом с данными, которые оно защищает. Сервис приложения тонкий: загрузил агрегат → вызвал order.Place() → сохранил.
Чек-лист при проектировании класса
Проектирование класса в DDD — это процесс определения структуры и поведения класса предметной области с учетом его роли в системе. При проектировании класса нужно сначала определить, является ли он сущностью или объектом-значением, какие у него будут инварианты, какие бизнес-методы он должен предоставлять, как будет обеспечиваться его целостность и как он впишется в агрегатную структуру. Класс должен иметь четкую ответственность, отражать концепцию предметной области и использовать единый язык. Важно проектировать класс с учетом его жизненного цикла: как он создается, как изменяется, как сохраняется и как удаляется. При проектировании нужно избегать излишней сложности, но в то же время предусмотреть все необходимые проверки и защиту инвариантов. Названия методов и полей должны быть взяты из языка предметной области, чтобы код был понятен как разработчикам, так и экспертам бизнеса.
- Это факт с ID и жизненным циклом? → Entity (возможно, корень агрегата).
- Это описание без собственной идентичности? → Value Object.
- Изменение должно быть атомарным с соседними объектами? → Включить в один агрегат, наружу — только корень.
- Операция не принадлежит одной сущности? → Domain Service.
- Нужно сообщить о свершившемся факте? → Domain Event.
- Нужно достать или сохранить агрегат? → Repository (интерфейс в домене).
- Создание сложное? → Factory или фабричный метод на корне.
Как применять карту типов DDD в ежедневной разработке
Карта типов DDD — это концептуальное представление всех строительных блоков, используемых при тактическом проектировании, и их взаимосвязей. Карта показывает, как сущности, объекты-значения, агрегаты, репозитории, фабрики, доменные сервисы, события и спецификации взаимодействуют друг с другом в рамках одного ограниченного контекста. На карте видно, что агрегат состоит из корня-сущности и дочерних сущностей с объектами-значениями, что репозиторий предоставляет интерфейс для сохранения и загрузки агрегатов, что фабрика создает сложные агрегаты, что доменные сервисы используются для операций между агрегатами, а события публикуются при изменениях состояния. Карта типов является важным артефактом для понимания архитектуры и помогает разработчикам не нарушать правила DDD, четко определяя, где какой тип класса должен использоваться.
Полезно воспринимать эту главу как таблицу маршрутизации для новых классов. Перед созданием файла задайте один вопрос: какую проблему решает будущий класс в предметной области.
Быстрый маршрут:
- Есть жизненный цикл и идентификатор — Entity.
- Есть смысл только в значении — Value Object.
- Требуется атомарная согласованность группы объектов — Aggregate.
- Операция охватывает несколько агрегатов — Domain Service.
- Нужна фиксация факта для реакций — Domain Event.
Практика ревью DDD-кода
Ревью DDD-кода — это процесс проверки кода на соответствие принципам и паттернам предметно-ориентированного проектирования, направленный на выявление нарушений архитектуры, неправильного использования строительных блоков и отхода от единого языка. При ревью обращают внимание на то, не содержится ли бизнес-логика вне сущностей и агрегатов, не нарушается ли принцип инкапсуляции через публичные сеттеры, не используется ли анемичная модель, правильно ли определены границы агрегатов, соблюдаются ли инварианты, корректно ли применяются репозитории и фабрики. Также проверяют, не попала ли в доменный слой инфраструктурная логика, не засорены ли сущности логикой представления, правильно ли названы классы и методы в соответствии с единым языком. Ревью DDD-кода требует понимания как предметной области, так и технических аспектов DDD, и часто проводится совместно с экспертами предметной области.
На code review удобно проходить короткий чек:
- Где у агрегата граница и корень.
- Какие инварианты защищаются методами корня.
- Нет ли утечки инфраструктуры в домен.
- Интерфейсы репозиториев выражены на языке домена.
- События называют свершившиеся факты в прошедшем времени.
Частые ловушки
- Добавление сеттеров в Entity "для удобства ORM".
- Размещение бизнес-правил в
Handlerпри пустом домене. - Смешение
Domain EventиIntegration Eventв одном контракте. - Слишком крупные агрегаты с высокой конкуренцией записей.
Сеттер — это метод класса, предназначенный для установки значения свойства объекта. В классическом подходе сеттер используется повсеместно для изменения данных, но в DDD публичные сеттеры считаются вредной практикой, потому что они нарушают инкапсуляцию и позволяют изменять состояние объекта без выполнения бизнес-правил и проверок. Сеттер заменяет сложную бизнес-логику простым присваиванием, что делает объект уязвимым для некорректных изменений. Вместо сеттеров в DDD следует использовать бизнес-методы, которые выражают намерение и содержат всю необходимую логику. Например, вместо метода setStatus, который может установить любой статус, следует использовать методы pay, cancel, ship, которые включают проверки на допустимость перехода, защиту инвариантов и генерацию событий. Если сеттер все же используется, он должен быть приватным или защищенным, и вызываться только внутри объекта или через рефлексию при маппинге.
Контракт в DDD — это формальное соглашение о взаимодействии между различными частями системы, обычно описывающее входные и выходные данные для операций, интерфейсы между слоями, спецификации API или интеграционные протоколы. Контракты определяют, какие команды и запросы принимает Application Service, какие DTO ожидаются на входе и возвращаются на выходе, какие события публикуются при выполнении операций. Они служат границами между слоями, контекстами или системами, обеспечивая независимость изменений по разные стороны контракта. Например, контракт REST API определяет URL, метод HTTP, формат тела запроса и структуру ответа. Контракт между контекстами может определять формат и содержание интеграционных событий. Строгое соблюдение контрактов позволяет развивать систему по частям без опасения сломать зависимые компоненты.
Конкуренция записей — это проблема, возникающая в многопользовательских системах, когда несколько пользователей или процессов пытаются одновременно изменить один и тот же агрегат, что может привести к потере данных или нарушению целостности. В DDD эта проблема особенно актуальна для агрегатов, которые должны быть транзакционно согласованы. Решение обычно достигается одним из двух способов: оптимистическая блокировка с версионированием, когда при сохранении проверяется, не изменил ли кто-то агрегат с момента его загрузки, и пессимистическая блокировка с явной блокировкой записи в базе данных. В DDD предпочтение обычно отдается оптимистической блокировке, так как она лучше подходит для систем с невысокой конкуренцией и позволяет избежать длительных блокировок. При обнаружении конфликта система должна сообщить пользователю об ошибке и предложить повторить операцию с обновленными данными.
См. также
- Доменная модель — инварианты, слои, ORM
- Классификация типов классов в ООП — Controller, DTO, Mapper вне DDD
- Паттерны проектирования — таблица паттернов DDD и распределённые паттерны
- Системный подход · Имитационное моделирование — проверка поведения системы до реализации
- Event Storming — workshop для выделения событий и агрегатов