DDD Aggregate с потенциально большой коллекцией с важным инвариантом

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

У меня есть вариант использования, который должен защищать свои инварианты, но также приведет к большой коллекции.

Агрегат — это Поставщик, и он может иметь несколько активных Рекламных акций. У каждой Promotion есть PromotionType, StartDate и EndDate. Инварианты:

  • в любой момент времени может быть максимум одно продвижение каждого PromotionType
  • в любой момент времени может быть максимум 2 акции
public Vendor : Aggregate {
    public Guid Id;
    public List<Promotion> Promotions;
    // some other Vendor props here

    public void AddPromotion(Promotion promo) {
        // protect invariants (business rules) here:
        // rule_1: if 2 promotions are already active during any time between promo.Start and promo.End then throw ex
        // rule_2: if during any time between promo.Start and promo.End there is promo with same Type then throw ex

        // if all is ok (invariants protected) then:
        Promotions.Add(promo);
    }
}

public Promotion : ValueObject {
    public PromotionType Type; // enum CheapestItemForFree, FreeDelivery, Off10PercentOfTotalBill
    public DateTime Start;
    public DateTime End;
}

Как мы видим, коллекция Promotions будет увеличиваться по мере добавления новых акций с течением времени, а срок действия старых акций истечет.

решение 1) Одна из возможностей состоит в том, чтобы сделать Promotion агрегатом, содержащим VendorId, но в этом случае будет сложно защитить упомянутые инварианты.

решение 2) Другая возможность состоит в том, чтобы иметь задание обслуживания, которое переместит срок действия (EndDate передан) в некоторую таблицу истории, но это вонючее решение IMO.

решение 3) Еще одна возможность состоит в том, чтобы также сделать Promotion агрегатом, но защитить инварианты в доменной службе, например:

public class PromotionsDomainService {
    public Promotion CreateNewVendorPromotion(Guid vendorId, DateTime start, DateTime end, PromotionType type) {
        // protect invariants here:
        // invariants broken -> throw ex
        // invariants valid -> return new Promotion aggregate object
    }
}

... но защищая его в PromotionsDomainService (и возвращая агрегаты), мы рискуем состоянием гонки и несогласованностью (если только мы не применим пессимистическую блокировку).

Какой рекомендуемый подход DDD в таком случае?


person Maciej Pszczolinski    schedule 11.02.2020    source источник
comment
Может ваш агрегат это PromotionPeriod?))   -  person DmitriBodiu    schedule 12.02.2020
comment
если AR — это PromotionPeriod, то вы не сможете сохранить инварианты без блокировки (пессимистичной или оптимистичной).   -  person Maciej Pszczolinski    schedule 13.02.2020
comment
какой инвариант вы не сможете сохранить?   -  person DmitriBodiu    schedule 13.02.2020
comment
оба упомянутых инварианта (в любой момент времени может быть максимум одно продвижение каждого типа продвижения, и в любой момент времени может быть максимум 2 продвижения).   -  person Maciej Pszczolinski    schedule 13.02.2020
comment
Таким образом, PromotionPeriod может защитить их обоих. Почему нет   -  person DmitriBodiu    schedule 13.02.2020


Ответы (2)


Ваш агрегат должен содержать только те данные, которые вам нужны для выполнения своей цели. Читая описание вашей проблемы, я не вижу, что Продавцу нужны просроченные акции для чего-либо. Поэтому вам нужно только сохранить активные акции в коллекции.

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

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

Обновление:

Прочитав еще раз вопрос, я понял, что даже не нужно держать по одной акции каждого типа. У вас будет максимум 2 акции в коллекции, поэтому размер коллекции будет максимум 2, если я не ошибаюсь.

person Francesc Castells    schedule 13.02.2020
comment
Таким образом, ключевая концепция здесь заключается в том, что во время AddPromotion я могу очищать просроченные акции, а история акций (которая мне действительно нужна) должна быть в другой таблице, в модели чтения. Однако модель чтения — это всего лишь проекция модели записи из источника, поэтому история IMO также должна быть частью модели записи. Таким образом, если другая таблица ссылается на продвижение (будь то активное или историческое), будет невозможно иметь правильное отношение в БД (не может иметь FK для двух разных таблиц). Решение, которое я вижу здесь, состоит в том, чтобы прервать обновление одного агрегата для каждого правила транзакции, а во время обработчика AddPromotion... - person Maciej Pszczolinski; 14.02.2020
comment
... Я обновлю оба агрегата Vendor со списком активных рекламных акций, а также создам новую сущность типа PromotionHistoryItem (также AR). - person Maciej Pszczolinski; 14.02.2020
comment
Может быть, я не должен был называть это прочитанной моделью, а просто историей продвижения. Эта таблица может быть источником достоверной информации об истории рекламных акций, а поставщик является органом, разрешающим создавать рекламные акции. Кроме того, с моделью, как она есть сейчас, продвижение является объектом-значением, поэтому вы не можете ссылаться на него по определению, и даже если это была сущность с идентификатором, вы не должны иметь FK для него напрямую, как это было бы против правило ссылки только на совокупные корни. Я бы не беспокоился о транзакциях, поскольку все, что ссылается на эту информацию, вполне может быть в отдельной базе данных/микросервисе. - person Francesc Castells; 14.02.2020
comment
Хорошо, этот подход имеет смысл. Спасибо! - person Maciej Pszczolinski; 14.02.2020
comment
@FrancescCastells Я так понимаю, вам также нужны рекламные акции, которые будут активированы в будущем, чтобы избежать добавления промоакции (которая также будет активна в будущем) с конфликтом типа промоакции или если в эту дату начала/окончания уже есть 2 акции диапазон (максимальные активы акции). - person jlvaquero; 14.02.2020
comment
@jlvaquero ах, это может быть так, и тогда в совокупности будет определенно больше двух рекламных акций. Но, не зная точного сценария, трудно сказать, будет ли это проблематично. Если продолжительность рекламных акций составляет несколько дней, вам, вероятно, потребуется много месяцев рекламных акций, чтобы это стало проблемой. Если они порядка минут, то может быть. Мое предложение делает решение не постоянно растущим, что рано или поздно сделает его проблемой. - person Francesc Castells; 14.02.2020

Это интересный случай, так как я всегда боролся с тем, почему совокупный корень когда-либо нуждался в сущности. Я склонен отдавать предпочтение объектам значений в агрегатах, которые ссылаются на другие агрегаты по идентификатору, но я думаю, что здесь у вас может быть сущность.

Решение может заключаться в том, чтобы иметь возможность регистрировать рекламные акции только в пределах Vendor, тем самым применяя инвариант. VendorRepository будет загружать только активные рекламные акции и добавлять их в Vendor. Таким образом, срок их действия может истечь в любое время, но репозиторий будет загружать только соответствующие рекламные акции.

Для бессрочных рекламных акций (которые вам, вероятно, не обязательно нужны) вы даже можете истечь за пределами Vendor, и ваши инварианты все равно должны быть удовлетворены.

Даже если вы выберете Promotion в качестве объекта-значения, что сработает, вы все равно можете следовать этому подходу.

person Eben Roux    schedule 13.02.2020
comment
Спасибо, Эбен. Это интересный подход. Однако - не будет ли анти-шаблон загружать только частичную коллекцию в коллекцию Vendor.Promotions? Тот факт, что коллекция не является полной, является неявным (скрытым) от потребителя объекта Vendor, и передовой практикой IMO является явное указание вещей. - person Maciej Pszczolinski; 13.02.2020
comment
Целью модели предметной области является обеспечение соблюдения ваших инвариантов, поэтому я не рассматриваю ее как какую-либо форму анти-шаблона. Другим примером может быть ActiveOrders на Customer, когда никому, конечно, не понадобится или когда-либо понадобится вся коллекция исторических ордеров. Иногда существуют своего рода технические трюки, которые мы можем применить, чтобы удовлетворить инварианты. Фильтрация коллекций или подсчетов может быть полезной время от времени. Со временем мы можем найти лучшие/улучшенные способы делать то же самое, но мы должны быть прагматичными в этих вещах. Надеюсь, это поможет :) - person Eben Roux; 13.02.2020
comment
Хорошо, я не думал, что отфильтрованная коллекция приемлема. Никогда не встречал такого подхода ни в статьях, ни в туториалах, ни в подкастах. Спасибо за этот совет :) - person Maciej Pszczolinski; 13.02.2020