Как разделять агрегат в DDD, чтобы его части оставались небольшими и управляемыми?

«Как разделять агрегат в DDD, чтобы его части оставались небольшими и управляемыми?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В Domain-Driven Design (DDD) большой агрегат — это признак плохой модели, ведущий к проблемам с конкурентным доступом и производительностью. Вот как я подхожу к его разделению:

  1. Пересмотр инвариантов: Первым делом проверяю, действительно ли все сущности внутри агрегата должны изменяться вместе как единое целое. Часто «большой» агрегат пытается защитить инварианты, которые можно обеспечить иначе.

  2. Выделение Value Objects: Логику, не требующую идентификации, выношу в Value Objects. Например, Address, Money (с валютой и суммой) или OrderLine (продукт, количество, цена). Это уменьшает ответственность корня агрегата.

    // Вместо полей в агрегате Order:
    // private String street; private String city; ...
    // Используем Value Object:
    class Order {
        private ShippingAddress address; // Value Object
    }
    class ShippingAddress {
        private final String street;
        private final String city;
        // Логика валидации адреса инкапсулирована здесь
    }
  3. Создание отдельных агрегатов по идентификатору: Если часть данных имеет собственный жизненный цикл или может изменяться независимо, это кандидат в новый агрегат. Связь между ними поддерживается через идентификатор, а не ссылку на объект.

    • До: Order содержит коллекцию объектов Payment.
    • После: Order содержит orderId. Payment — отдельный агрегат со ссылкой orderId. Изменение статуса платежа не требует блокировки всего заказа.
  4. Использование ссылок на другие агрегаты: Вместо загрузки всего связанного агрегата корень хранит только его ID. Данные загружаются при необходимости через репозиторий. Это четко очерчивает границы транзакции.

  5. Event-Driven подход: Если согласованность в реальном времени не критична, можно разбить процесс на этапы, используя доменные события. Например, после создания Order публикуется OrderPlacedEvent, который обрабатывается асинхронно другими контекстами (резервирование товара, отправка уведомления).

Ключевой принцип: агрегат должен быть как можно меньше, но при этом оставаться консистентным. Ориентир — чтобы он помещался в память и изменялся одной транзакцией без блокировок на уровне БД.