День 1254. #DDD
Обязанности Агрегатов в DDD
Агрегат происходит из DDD и предоставляет способ инкапсулировать бизнес-логику в нескольких связанных объектах.
Основные правила агрегатов:
1. Каждый агрегат имеет один корневой объект. Агрегаты называются по их корневому объекту.
2. Дочерние элементы агрегатов не сохраняются отдельно, а только как часть агрегата. Более того, доступ к ним также должен осуществляться только через агрегат.
Например, навигационные свойства должны существовать только от корней агрегатов к дочерним элементам (но не наоборот и не между агрегатами). Т.е. корень должен иметь возможность обращаться к любому из своих дочерних элементов, а дочерние элементы обычно имеют только свойство ID своего родителя.
3. Корень отвечает за валидность всего агрегата
Это не означает, что весь код должен находиться в корневом объекте. Корень может делегировать полномочия своим дочерним элементам при выполнении задач, за которые он отвечает.
Рассмотрим простой пример сущности Order, которая имеет коллекцию сущностей OrderItem. Order — корень:
public class Order
{
// прочие свойства и методы опущены
public IEnumerable<OrderItem> OrderItems { get; private set;}
}
public class OrderItem
{
// прочие свойства и методы опущены
public int ProductId { get; set; }
public int Quantity { get; set; }
}
Некоторые типичные операции агрегата:
- создание нового заказа,
- добавление OrderItem в заказ,
- изменение количества в OrderItem.
Некоторые бизнес-правила:
- валидный заказ имеет хотя бы один OrderItem,
- валидная коллекция OrderItems не содержит повторяющихся ProductId,
- валидный OrderItem имеет положительное количество.
Очевидно, что Order здесь отвечает за первые 2 бизнес-правила, а вот за валидацию количества (и, возможно, других своих свойств) может (и должен) отвечать OrderItem. Можно, конечно, пойти дальше и использовать для свойства Quantity тип, не допускающий отрицательных значений, но сегодня не об этом.
Order, отвечающий за валидность всего агрегата, может при этом использовать поведение OrderItem. Ему не нужно проверять OrderItem извне, если он может быть уверен, что дочерний объект валиден. Вам же не нужно проверять, что свойство Month типа DateTime меньше 13. Тип делает эту проверку за вас, поэтому вы можете быть уверены, что месяц будет в допустимом диапазоне, если у вас есть экземпляр типа.
Многие разработчики также считают, что правило агрегата, гарантирующее, что весь агрегат является «действительным», требует метода для проверки достоверности. Иногда это полезно, но лучший дизайн — это тот, который изначально не допускает существования недопустимых значений. Снова рассмотрим тип DateTime. У него нет метода IsValid(), который вы вызываете после создания экземпляра, указав год, месяц и день как 2022, 50 и 50. Тип просто не позволит вам установить свои месяц или день в значение 50. Если вам удастся создать экземпляр DateTime, вы можете быть уверены, что он валиден, а также вы не сможете использовать ни один из его методов, чтобы поместить его в недопустимое состояние. Этот же подход нужно использовать со своими агрегатами и сущностями.
Итого
Корень агрегата отвечает за то, чтобы агрегат находился в допустимом состоянии. Но это не означает, что каждая операция, выполняемая над любым элементом агрегата, должна находиться в корне агрегата. Это также не означает, что агрегат должен иметь метод, выполняющий проверку. Использование надлежащего объектно-ориентированного дизайна и обеспечение того, чтобы ваши объекты инкапсулировали и управляли своим собственным состоянием, обеспечивает более чистый дизайн.
См. также Сущности и Объекты-Значения
Источник: ardalis.com/aggrega…y-design