Ответ
Большой агрегат (Aggregate Root с множеством сущностей и value objects) часто возникает из-за нарушения принципов проектирования границ контекста и инвариантов.
Основные причины:
- Неверно определенные инварианты консистентности. Агрегат должен защищать инварианты только в своих границах. Если вы пытаетесь обеспечить консистентность между сущностями, которые могут изменяться независимо, это признак того, что они должны быть разными агрегатами.
- Страх ссылок между агрегатами. Разработчики иногда включают всё в один агрегат, чтобы избежать использования
IDдля ссылок и обеспечения eventual consistency. - Агрегат как отражение таблицы БД. Это антипаттерн. Агрегат — это граница транзакции, а не схема данных.
- Нарушение принципа единственной ответственности (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+ 🔞
Давай разберем эту дичь, а то у меня уже глаза на лоб лезут. Видишь ли, вон тот здоровенный агрегат, который у тебя получился — это классический случай, когда разработчик начитался умных книжек, а потом на проекте подозрение ебать чувствую, что он просто всё в одну кучу сгребает.
Основные причины, почему так выходит — ёперный театр просто:
- Инварианты консистентности определены через жопу. Ты пытаешься в одном агрегате обеспечить святость между сущностями, которые, по жизни, должны жить отдельно. Это как пытаться одной транзакцией гарантировать, что пока ты в интернет-магазине заказ оформляешь, на складе у кладовщика рука не дрогнет и он эту последнюю видеокарту не продаст. Доверия ебать ноль между ними, и правильно! Выделяй это в отдельные агрегаты и живи спокойно.
- Страх перед eventual consistency. Многие так бздят использовать ID для ссылок между агрегатами, что проще запихнуть всё в один монолит. Мол, так надёжнее. Ага, надёжнее, пока этот монолит не превратится в манду с ушами, которую никто не может понять или изменить.
- Отражение таблицы БД в коде. Вот это вообще пизда рулю. Ты смотришь на схему базы, видишь кучу связей один-ко-многим и думаешь: "А, ну это явно один агрегат!" Э, бошка, думай! Агрегат — это граница транзакции для бизнес-правил, а не твоя ER-диаграмма.
- Нарушение 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) { ... } // И склад сам решает, как это делать
}
Главное правило, которое нужно выжечь на подкорке: Агрегат должен быть настолько маленьким, насколько это возможно, но при этом достаточно большим, чтобы все его внутренние правила (инварианты) выполнялись за одну транзакцию. Не больше, не меньше. Всё остальное — от лукавого и ведёт к овердохуище проблем.