Типы классов в DDD
Модель и масштаб — Основы БД, опорные темы, проектирование БД, пакетная работа. Карта — о разделе.
В доменной модели описаны смысл и правила предметной области. В классификации типов классов в ООП — общие архетипы (Controller, Repository, DTO). DDD (Domain-Driven Design) задаёт отдельный набор типов классов доменного слоя: объекты с идентичностью, объекты-значения, агрегаты, сервисы и события. Их имена и границы должны совпадать с общим языком (Ubiquitous Language), которым говорят разработчики и эксперты предметной области.
После Доменной модели и до углубления в Паттерны проектирования (таблица DDD-паттернов).
Для стратегического DDD (bounded context, контекстные карты) см. ту же главу про домен и раздел про декомпозицию в Стратегии декомпозиции монолитных систем.
Два слоя — домен и приложение
Путаница чаще всего возникает между доменным и прикладным (application) слоями.
| Слой | Кто координирует | Примеры классов |
|---|---|---|
| Домен | Правила предметной области | Order, Money, OrderPlaced, TransferService (доменный) |
| Прикладной | Сценарии использования, транзакции, вызов инфраструктуры | PlaceOrderHandler, PlaceOrderCommand, оркестрация репозиториев |
| Инфраструктура | БД, HTTP, очереди | OrderRepository (реализация), SmtpEmailSender |
| Представление | Ввод-вывод | OrdersController, OrderDto |
Доменный сервис выражает операцию на языке бизнеса (TransferMoney, CalculateShipping). Сервис приложения (use case) загружает агрегаты, вызывает домен, сохраняет изменения и публикует интеграционные события — без дублирования бизнес-правил.
Карта типов классов в DDD
| Тип | Идентичность | Изменяемость | Типичное имя в коде | Слой |
|---|---|---|---|---|
| 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 (сущность)
Сущность — объект, который определяется устойчивым идентификатором, а не текущим набором полей. Пользователь с id = 42 остаётся тем же пользователем после смены email.
Признаки в коде:
- поле
Id(или value objectCustomerId); - методы, меняющие состояние с проверкой инвариантов (
ConfirmEmail(),Block()); - равенство по ID: два экземпляра с одним ID считаются одной сущностью.
Код ITЗагрузка примера кода…
Сущность может быть корнем агрегата или внутренним элементом агрегата (см. ниже).
Value Object (объект-значение)
Объект-значение описывает характеристику без собственной жизни в системе — деньги, адрес, диапазон дат, координаты. Два значения с одинаковыми полями взаимозаменяемы.
Практика:
- неизменяемость (
record,readonly struct, финальные поля); - валидация в конструкторе (
Amount > 0, формат email); - равенство по всем значимым полям;
- отсутствие суррогатного ID в БД (часто встраивается в таблицу сущности как колонки).
Код ITЗагрузка примера кода…
Значения удобно передавать между слоями внутри домена; наружу (в API) чаще отдают примитивы или DTO, собранные из value objects.
Aggregate и Aggregate Root (агрегат и корень)
Агрегат — кластер сущностей и значений, который изменяется как единое целое в одной транзакции. Корень агрегата (Aggregate Root) — единственная сущность, на которую ссылаются извне.
Правила:
- внешний код вызывает только методы корня (
order.AddLine(...), а неorder.Lines[0].Quantity = 5); - инварианты проверяются внутри границы агрегата;
- репозиторий загружает и сохраняет агрегат целиком, а не отдельные строки без корня;
- ссылки между агрегатами — по ID, а не по прямым объектным ссылкам (избегают "большого комка грязи").
Пример границы: Order (корень) + коллекция OrderLine + возможно DeliveryAddress как value object. Отдельный агрегат Product на складе — другая граница; заказ хранит ProductId, а не живой объект Product.
Размер агрегата держат небольшим: один бизнес-инвариант на транзакцию. Слишком крупный агрегат ведёт к блокировкам и конфликтам при конкурентной записи.
Domain Service (доменный сервис)
Доменный сервис — операция предметной области, которую неудобно отнести к одной сущности:
- затрагивает несколько агрегатов (
TransferBetweenAccounts); - зависит от внешнего правила, но формулируется на языке домена (
ShippingCostCalculatorс тарифами как входными данными); - чистая политика без состояния (
UniqueUsernamePolicy— интерфейс в домене, реализация с запросом к БД в инфраструктуре).
Доменный сервис не знает про HTTP, EF Core, Kafka. Он принимает и возвращает доменные типы.
Domain Event (доменное событие)
Доменное событие фиксирует факт, который уже произошёл в предметной области: OrderPlaced, InvoiceIssued. Имя — в прошедшем времени. Событие неизменяемо и содержит данные, нужные подписчикам (ID агрегата, сумма, метка времени).
Типичный поток:
- метод корня агрегата меняет состояние и добавляет событие в коллекцию
DomainEvents; - слой приложения после успешного
Saveпубликует события в шину или вызывает обработчики; - побочные эффекты (email, аналитика) живут в обработчиках, а не внутри сущности.
События помогают развязать подсистемы и строить event sourcing там, где нужен полный аудит изменений.
Repository (репозиторий)
Репозиторий в DDD — абстракция коллекции агрегатов в доменном слое — GetById, Add, иногда Find по спецификации. Методы называют по-русски/английски в терминах домена: FindOpenOrdersByCustomer, а не GetAllFromTableOrders.
- интерфейс
IOrderRepositoryобъявляют в домене; - реализацию с ORM, SQL, кэшем помещают в инфраструктуру;
- репозиторий возвращает полный агрегат, готовый к вызову доменных методов.
Репозиторий не содержит бизнес-правил ("можно ли отменить заказ" — в Order.Cancel()).
Factory (фабрика)
Фабрика инкапсулирует сложное создание агрегата, когда конструктор раздувается или нужны проверки на нескольких источниках:
OrderFactory.CreateFromCart(cart, customerId);- сборка агрегата из нескольких value objects с согласованностью.
Простые сущности создают через конструктор или статический метод Create на самой сущности; фабрика — для нетривиальных правил инициализации.
Specification (спецификация)
Спецификация выносит правило отбора или допустимости в отдельный объект: 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) — это разделение слоёв, а не дублирование логики.
Мини-практикум по классификации классов
Ниже короткие учебные вопросы для самостоятельной проверки:
Priceс полямиAmountиCurrencyбез ID — это Entity или Value Object?Orderс коллекциейOrderLine, где внешние вызовы идут только черезOrder— это агрегат или просто сущность?CalculateDeliveryCostиспользует адрес, вес и тарифы, но не хранит состояние — это Domain Service или Application Service?OrderPlacedсOrderIdиOccurredAt— это Domain Event или DTO?
Рекомендуемый порядок ответа:
- сначала определить границу ответственности;
- затем идентичность и жизненный цикл;
- затем место класса в слоях архитектуры.
Если классификация даётся с трудом, возвращайтесь к Доменной модели и Построению систем на основе классов и объектов.
Связь с "анемичной моделью"
Анемичная модель — классы только с get/set и вся логика в "сервисах". Для DDD это потеря главного преимущества — инварианты размазаны, состояние можно испортить, минуя домен.
Здоровая модель: поведение рядом с данными, которые оно защищает. Сервис приложения тонкий: загрузил агрегат → вызвал order.Place() → сохранил.
Чек-лист при проектировании класса
- Это факт с ID и жизненным циклом? → Entity (возможно, корень агрегата).
- Это описание без собственной идентичности? → Value Object.
- Изменение должно быть атомарным с соседними объектами? → Включить в один агрегат, наружу — только корень.
- Операция не принадлежит одной сущности? → Domain Service.
- Нужно сообщить о свершившемся факте? → Domain Event.
- Нужно достать или сохранить агрегат? → Repository (интерфейс в домене).
- Создание сложное? → Factory или фабричный метод на корне.
Как применять карту типов DDD в ежедневной разработке
Полезно воспринимать эту главу как таблицу маршрутизации для новых классов. Перед созданием файла задайте один вопрос: какую проблему решает будущий класс в предметной области.
Быстрый маршрут:
- Есть жизненный цикл и идентификатор — Entity.
- Есть смысл только в значении — Value Object.
- Требуется атомарная согласованность группы объектов — Aggregate.
- Операция охватывает несколько агрегатов — Domain Service.
- Нужна фиксация факта для реакций — Domain Event.
Практика ревью DDD-кода
На code review удобно проходить короткий чек:
- Где у агрегата граница и корень.
- Какие инварианты защищаются методами корня.
- Нет ли утечки инфраструктуры в домен.
- Интерфейсы репозиториев выражены на языке домена.
- События называют свершившиеся факты в прошедшем времени.
Частые ловушки
- Добавление сеттеров в Entity "для удобства ORM".
- Размещение бизнес-правил в
Handlerпри пустом домене. - Смешение
Domain EventиIntegration Eventв одном контракте. - Слишком крупные агрегаты с высокой конкуренцией записей.
См. также
- Доменная модель — инварианты, слои, ORM
- Классификация типов классов в ООП — Controller, DTO, Mapper вне DDD
- Паттерны проектирования — таблица паттернов DDD и распределённые паттерны
- Системный подход · Имитационное моделирование — проверка поведения системы до реализации
- Event Storming — workshop для выделения событий и агрегатов