Агрегат
Агрегат (aggregate) — паттерн из Domain Driven Design. Представляет собой дерево связанных сущностей - объектов предметной области. Доступ ко всем объектом осуществляется только через один главный объект - “корень агрегата”.
Объекты снаружи должны ссылаться только на корень агрегата и никогда на остальные объекты, которые в него входят. Корень агрегата отвечает за поддержание целостности всего агрегата.
Пример агрегата¶
class Order {
private Integer id;
private List<Item> items;
private Integer sum;
private DiscountPolicy discountPolicy;
public void addItem(Item item) {
items.add(item);
var itemFinalPrice = discountPolicy.applyDiscount(item.getPrice());
sum += itemFinalPrice;
}
public Integer getSum() {
return sum;
}
}
В данном примере класс Order - корень агрегата, все изменения в корзине заказа (items) и итоговой стоимости (sum) производятся только в нем. В классе нет setter’ов, через которые можно было бы нарушить его целостность (например, добавив товар в корзину без пересчета суммы).
Вместо того, чтобы размазывать бизнес логику и проверки состояния по разным классам мы помещаем их в один класс, непосредственно рядом самими данными.
Правила проектирования агрегатов¶
- Агрегат также можно рассматривать как границу транзакции в БД и минимальную единицу хранения. При запросе агрегата из БД он загружается целиком, также целиком сохраняется по завершению работы с ним. Поэтому для улучшения производительности агрегат должен быть как можно меньше.
- Агрегат может ссылаться на другой агрегат только по id. Не допускается модификация одного агрегата через вызов метода из корня другого агрегата.
- Обмениваться информацией агрегаты могут через доменные события, придерживаясь eventual consistency.
- Необходимо стремиться к тому, чтобы в рамках одной транзакции менялся только один агрегат. Если появляется требование изменить несколько агрегатов в рамках одной транзакции, то стоит задуматься. Возможно границы агрегатов были выбраны неверно, или в доменной области существует еще не открытая разработчиком новая концепция, новый агрегат. Нарушая это правило мы теряем возможность хранить агрегаты в разных БД. Как следствие, не сможем, в случае необходимости, разнести по разным сервисам.
- Разработчик должен контролировать одновременный доступ к агрегату из разных потоков и сервисов. Например, через оптимистичную или пессимистичную блокировку на уроне БД.
Чем полезен паттерн агрегат?¶
Используя паттерн агрегат и придерживаясь приведенных выше ограничений мы получаем следующие свойства:
- Эффективная работа с NoSQL БД. Так как агрегат уже является границей транзакции, то мы можем загружать и сохранять его целиком в БД, что хорошо ложиться на концепцию NoSQL БД.
- Удобно контролировать конкурентный доступ. Загружая агрегат можно наложить пессимистичную или оптимистичную блокировку на корень агрегата и косвенно заблокировать все объекты входящие в агрегат.
- Потенциал к масштабированию хранения данных агрегата. За счет того, что агрегат является атомарной единицей бизнес логики и данных, и ссылается на остальные агрегат только по уникальному в ID - это позволяет переносить данные агрегата из одной БД в другую, использовать шардирование и партиционирование, не боясь нарушить целостность.
- High cohesion. Бизнес правила и операции, которые относятся к одной сущности хранятся в одном месте, а не по всему коду в Service классах.
- Low coupling. Благодаря правилу “ссылаться на другой агрегат только по Id” - агрегаты слабо связаны друг другом.
Как определить, что граница агрегата выбрана правильно?¶
- Есть ли аналог в реальном мире?
- Агрегат не пересекает Bounded Context?
- Есть ли инвариант для этой группы объектов?
- Данные должны изменяться в рамках одной транзакции?
Когда использовать?¶
Агрегаты хорошо подходят для программировании сложной бизнес логики, поддержания целостности при конкурентном доступе к данным, для управления сложностью, повышения предсказуемости и тестируемости. Нет смысла использовать их в простых задачах, где это не требуется.
Если логика приложения больше похожа на CRUD, то стоит начать с подхода Transaction script. Если же логики стало слишко много и из за этого вам стало сложнее тестировать ее, то стоит задуматься над изоляцией доменной логики, в отдельный слой, например в domain service или в агрегаты.