DDD: тактический — entity, value object, aggregate, domain events
↑ 8.2 Архитектурные стили и DDD · ← Предыдущая · → Следующая
Зачем это на собесе
Практические строительные блоки DDD в C#: агрегат, инварианты, границы транзакции.
Объяснение
| Блок | Смысл |
|---|---|
| Entity | объект с идентичностью, живущий во времени (Order) |
| Value Object | неизменяемый объект без идентичности, сравнивается по значению (Money, Address, Email) |
| Aggregate | кластер объектов с общими инвариантами и одним корнем (aggregate root) |
| Repository | загрузка/сохранение агрегата целиком |
| Domain Service | логика, не принадлежащая одной сущности |
| Domain Event | факт, произошедший в домене (OrderPaid) |
| Factory | сложное создание агрегата |
public readonly record struct Money(decimal Amount, string Currency)
{
public static Money Zero(string c) => new(0, c);
public Money Add(Money o) => o.Currency == Currency ? this with { Amount = Amount + o.Amount } : throw new InvalidOperationException("currency mismatch");
}
public class Order // aggregate root
{
private readonly List<OrderItem> _items = [];
private readonly List<IDomainEvent> _events = [];
public Guid Id { get; } = Guid.NewGuid();
public Status Status { get; private set; } = Status.New;
public IReadOnlyList<OrderItem> Items => _items;
public IReadOnlyList<IDomainEvent> Events => _events;
public void AddItem(Sku sku, int qty, Money price)
{
if (Status != Status.New) throw new DomainException("Нельзя менять оплаченный заказ"); // инвариант защищён корнем
if (qty <= 0) throw new DomainException("Количество должно быть положительным");
_items.Add(new OrderItem(sku, qty, price));
}
public void Pay()
{
if (_items.Count == 0) throw new DomainException("Пустой заказ");
Status = Status.Paid;
_events.Add(new OrderPaid(Id));
}
}Правила агрегата:
- Изменения только через корень; снаружи нет прямого доступа к внутренним сущностям.
- Транзакция = один агрегат; между агрегатами — ссылки по идентификатору и eventual consistency (доменные события).
- Агрегат небольшой: маленькие агрегаты лучше масштабируются и меньше конфликтуют.
Доменные события публикуются после сохранения (через outbox) и запускают реакции в других агрегатах/контекстах.
Нюансы и подводные камни
- Анемичная модель: публичные сеттеры и логика в сервисах.
- Гигантские агрегаты (заказ + клиент + товары) — блокировки и медленная загрузка.
- Value Object как
recordудобен; EF Core умеет owned types/ComplexProperty. - Исключения для нарушения инварианта vs Result — определите единый стиль.
- Не тащите «DDD-лайт» ради терминов без пользы.
Практика
- Смоделируйте
Orderс инвариантами и value objects. - Реализуйте публикацию доменных событий через outbox.
- Найдите в проекте агрегат, который слишком велик, и разделите.
Вопросы с ответами
Что такое aggregate root?
Единственная точка входа в агрегат, отвечающая за сохранение инвариантов всего кластера объектов.
Entity или value object?
Если важна идентичность — entity; если важно только значение — неизменяемый value object.
Почему транзакция ограничена одним агрегатом?
Так проще масштабировать и избегать блокировок; консистентность между агрегатами — событийная.