Перейти к содержанию

Агрегат

Агрегат (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 или в агрегаты.

Статьи