DDD: тактический — entity, value object, aggregate, domain events

↑ 8.2 Архитектурные стили и DDD · ← Предыдущая · → Следующая

Обязательно~4 мин чтенияУровень: senior

Зачем это на собесе

Практические строительные блоки 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-лайт» ради терминов без пользы.

Практика

  1. Смоделируйте Order с инвариантами и value objects.
  2. Реализуйте публикацию доменных событий через outbox.
  3. Найдите в проекте агрегат, который слишком велик, и разделите.

Вопросы с ответами

Связанные темы