Почему при проектировании предметной области (DDD) может получаться слишком большой агрегат?

Ответ

Большой агрегат (Aggregate Root с множеством сущностей и value objects) часто возникает из-за нарушения принципов проектирования границ контекста и инвариантов.

Основные причины:

  1. Неверно определенные инварианты консистентности. Агрегат должен защищать инварианты только в своих границах. Если вы пытаетесь обеспечить консистентность между сущностями, которые могут изменяться независимо, это признак того, что они должны быть разными агрегатами.
  2. Страх ссылок между агрегатами. Разработчики иногда включают всё в один агрегат, чтобы избежать использования ID для ссылок и обеспечения eventual consistency.
  3. Агрегат как отражение таблицы БД. Это антипаттерн. Агрегат — это граница транзакции, а не схема данных.
  4. Нарушение принципа единственной ответственности (SRP). Агрегат начинает отвечать за слишком много бизнес-процессов.

Пример проблемы и решения:

Проблема: Агрегат Order управляет и заказом, и инвентарём.

// Слишком большой агрегат
class Order {
    private List<OrderLine> lines;
    private List<InventoryItem> inventoryItems; // Это должен быть отдельный агрегат!
    // ... методы для резервирования товара, изменения его количества и т.д.
}

Решение: Выделить InventoryItem в отдельный агрегат. Согласованность между Order и Inventory поддерживается через доменные события (Domain Events) или saga.

class Order {
    private UUID id;
    private List<OrderLine> lines;

    public OrderPlacedEvent place() {
        // ... логика размещения заказа
        return new OrderPlacedEvent(this.id, this.lines); // Событие для резервирования товара
    }
}

class InventoryItem {
    private UUID productId;
    private int quantity;

    public void reserveForOrder(UUID orderId, int amount) { ... }
}

Правило: Агрегат должен быть как можно меньше, но достаточным для поддержания инвариантов в рамках одной транзакции.

Ответ 18+ 🔞

Давай разберем эту дичь, а то у меня уже глаза на лоб лезут. Видишь ли, вон тот здоровенный агрегат, который у тебя получился — это классический случай, когда разработчик начитался умных книжек, а потом на проекте подозрение ебать чувствую, что он просто всё в одну кучу сгребает.

Основные причины, почему так выходит — ёперный театр просто:

  1. Инварианты консистентности определены через жопу. Ты пытаешься в одном агрегате обеспечить святость между сущностями, которые, по жизни, должны жить отдельно. Это как пытаться одной транзакцией гарантировать, что пока ты в интернет-магазине заказ оформляешь, на складе у кладовщика рука не дрогнет и он эту последнюю видеокарту не продаст. Доверия ебать ноль между ними, и правильно! Выделяй это в отдельные агрегаты и живи спокойно.
  2. Страх перед eventual consistency. Многие так бздят использовать ID для ссылок между агрегатами, что проще запихнуть всё в один монолит. Мол, так надёжнее. Ага, надёжнее, пока этот монолит не превратится в манду с ушами, которую никто не может понять или изменить.
  3. Отражение таблицы БД в коде. Вот это вообще пизда рулю. Ты смотришь на схему базы, видишь кучу связей один-ко-многим и думаешь: "А, ну это явно один агрегат!" Э, бошка, думай! Агрегат — это граница транзакции для бизнес-правил, а не твоя ER-диаграмма.
  4. Нарушение SRP. Агрегат начинает делать всё: и заказ обрабатывать, и инвентарь считать, и курьера вызывать. Получается такой полупидор, который за всё отвечает, но ничего нормально сделать не может.

Смотри на примере, как это бывает:

Проблема: Агрегат Order стал таким жирным, что тащит за собой ещё и инвентарь. Сам от себя охуел, когда это увидел.

// Слишком большой агрегат — пиздец, а не код
class Order {
    private List<OrderLine> lines;
    private List<InventoryItem> inventoryItems; // Ты чё, ёпта? Это ж отдельный агрегат должен быть!
    // ... и тут же методы, которые резервируют товар, списывают его и бог знает что ещё
}

Решение: Режем этого монстра без жалости. Выносим InventoryItem в свой собственный агрегат. А связь между заказом и складом делаем через доменные события. Пусть общаются как взрослые, а не сидят в одной песочнице.

class Order {
    private UUID id;
    private List<OrderLine> lines;

    public OrderPlacedEvent place() {
        // ... тут логика создания заказа
        return new OrderPlacedEvent(this.id, this.lines); // Кидаем событие: "Эй, склад, резервируй товар!"
    }
}

class InventoryItem {
    private UUID productId;
    private int quantity;

    public void reserveForOrder(UUID orderId, int amount) { ... } // И склад сам решает, как это делать
}

Главное правило, которое нужно выжечь на подкорке: Агрегат должен быть настолько маленьким, насколько это возможно, но при этом достаточно большим, чтобы все его внутренние правила (инварианты) выполнялись за одну транзакцию. Не больше, не меньше. Всё остальное — от лукавого и ведёт к овердохуище проблем.