Типы классов в DDD
В доменной модели описаны смысл и правила предметной области. В классификации типов классов в ООП — общие архетипы (Controller, Repository, DTO). DDD (Domain-Driven Design) задаёт отдельный набор типов классов доменного слоя: объекты с идентичностью, объекты-значения, агрегаты, сервисы и события. Их имена и границы должны совпадать с общим языком (Ubiquitous Language), которым говорят разработчики и эксперты предметной области.
После Доменной модели и до углубления в Паттерны проектирования (таблица DDD-паттернов). Для стратегического DDD (bounded context, контекстные карты) см. ту же главу про домен и раздел про декомпозицию в 104.
Два слоя — домен и приложение
Путаница чаще всего возникает между доменным и прикладным (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 считаются одной сущностью.
public sealed class Customer
{
public CustomerId Id { get; }
public Email Email { get; private set; }
public CustomerStatus Status { get; private set; }
public void ConfirmEmail(Email confirmed)
{
if (Status != CustomerStatus.Pending)
throw new DomainException("Подтверждение доступно только для ожидающих аккаунтов.");
Email = confirmed;
Status = CustomerStatus.Active;
}
}
Сущность может быть корнем агрегата или внутренним элементом агрегата (см. ниже).
Value Object (объект-значение)
Объект-значение описывает характеристику без собственной жизни в системе: деньги, адрес, диапазон дат, координаты. Два значения с одинаковыми полями взаимозаменяемы.
Практика:
- неизменяемость (
record,readonly struct, финальные поля); - валидация в конструкторе (
Amount > 0, формат email); - равенство по всем значимым полям;
- отсутствие суррогатного ID в БД (часто встраивается в таблицу сущности как колонки).
public readonly record struct Money(decimal Amount, string Currency)
{
public Money(decimal amount, string currency)
{
if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount));
if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException(nameof(currency));
Amount = amount;
Currency = currency;
}
public static Money operator +(Money left, Money right)
{
if (left.Currency != right.Currency)
throw new InvalidOperationException("Складывать можно только одну валюту.");
return new Money(left.Amount + right.Amount, left.Currency);
}
}
Значения удобно передавать между слоями внутри домена; наружу (в 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) — это разделение слоёв, а не дублирование логики.
Связь с «анемичной моделью»
Анемичная модель — классы только с get/set и вся логика в «сервисах». Для DDD это потеря главного преимущества: инварианты размазаны, состояние можно испортить, минуя домен.
Здоровая модель: поведение рядом с данными, которые оно защищает. Сервис приложения тонкий: загрузил агрегат → вызвал order.Place() → сохранил.
Чек-лист при проектировании класса
- Это факт с ID и жизненным циклом? → Entity (возможно, корень агрегата).
- Это описание без собственной идентичности? → Value Object.
- Изменение должно быть атомарным с соседними объектами? → Включить в один агрегат, наружу — только корень.
- Операция не принадлежит одной сущности? → Domain Service.
- Нужно сообщить о свершившемся факте? → Domain Event.
- Нужно достать или сохранить агрегат? → Repository (интерфейс в домене).
- Создание сложное? → Factory или фабричный метод на корне.
См. также
- Доменная модель — инварианты, слои, ORM
- Классификация типов классов в ООП — Controller, DTO, Mapper вне DDD
- Паттерны проектирования — таблица паттернов DDD и распределённые паттерны
- Системный подход · Имитационное моделирование — проверка поведения системы до реализации
- Event Storming — workshop для выделения событий и агрегатов
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). 49 элементов 8 элементов Обычно проектирование применяется к каким-то планам, схемам, моделям или расчётам, которые описывают будущий объект, включая характеристики, функции, инженерные решения. Архитектурные решения, касающиеся распределения компонентов и организации их взаимодействия, определяют фундаментальные свойства системы: её масштабируемость, отказоустойчивость, сложность. Это достигается через инверсию зависимостей — принцип, согласно которому высокоуровневые модули не должны зависеть от низкоуровневых; оба должны зависеть от абстракций. Компонентно-ориентированная архитектура - согласованность версий общих модулей и управление зависимостями между сервисами. Как резать монолит без «большого взрыва»: анализ, Strangler, DDD-контексты, данные, саги и метрики успеха. Инфраструктура — это множество решений, инкапсулированных в сервисы, каждое из которых накладывает ограничения и открывает возможности. Классификация типов классов в ООП - семантика имён, роли объектов и разделение ответственности в проекте. Построение систем на классах и объектах - модель предметной области, границы ответственности и связи между сущностями. Доменная модель - как отразить предметную область в ПО, выделить сущности и зафиксировать правила бизнес-логики. В практике разработки программного обеспечения естественным образом возникают типовые задачи: как управлять жизненным циклом объекта?Проектирование
Паттерны проектирования
Основы проектирования и архитектуры программного обеспечения
Архитектурные стили и их применение
Стили внутренней организации кода
Принципы компонентно-ориентированной архитектуры
Стратегии декомпозиции монолитных систем
Влияние инфраструктуры на архитектурные решения
Классификация типов классов в объектно-ориентированном проектировании
Построение систем на основе классов и объектов
Доменная модель
Паттерны проектирования