DDD применяет агрегированные инварианты при изменении состояния дочернего объекта

#c# #domain-driven-design

#c# #дизайн, управляемый доменом

Вопрос:

У меня есть Contract объект, который имеет DateRange свойство (DateFrom, DateTo) и Sales коллекцию.

У каждого Sale также есть DateRange свойство, которое должно находиться внутри границ Contract ‘s DateRange .

Каков правильный способ применения вышеуказанного инварианта при изменении Sale даты?

 public class Contract : Entity
{
    public DateRange Dates { get; private set; }
    public ICollection<Sale> Sales { get; private set; }
}

public class Sale : Entity
{
    public DateRange Dates { get; private set; }

    public void ChangeDates(DateRange dates)
    {
        Dates = dates;
    }
}
  

Редактировать

Contract Даты могут меняться в любое время, поэтому каждая Sale из них должна быть изменена соответствующим образом.

Ответ №1:

На основе ваших текущих требований

Интерпретируя ваши требования, Contract является корнем агрегата и Sale является объектом в Contract агрегате. Поскольку требование заключается в том, что любые даты продажи ДОЛЖНЫ находиться в пределах набора дат контракта, любое изменение даты продажи должно управляться Контрактом, поэтому он может сначала проверить даты контракта.

Для этого у вас должен быть метод Contract , например:

 public void ChangeSaleDate(long SaleId, DateRange dates)
{
    if (this.Dates.Surround(dates))
    {
        var sale = this.Sales.First(s => s.Id == SaleId);
        sale.ChangeDates(dates);
    }
    else
    {
        throw new ArgumentException("New Sale dates must be between ...", "dates");
    }
}
  

Это предполагает, что у вас есть SaleId — или какой-либо другой способ идентификации продажи в рамках контракта, и что вы внедрили Surround метод DateRange для поддержки такого рода проверки.

В зависимости от структуры вашего проекта вы также можете пометить ChangeDates метод Sale как internal , чтобы убедиться, что вы случайно не вызовете его из своих служб приложений.

Из вашего комментария верно, что этот механизм может привести к большому количеству методов в aggregate root ( Contract ), поскольку он применяет инварианты, которые применяются ко «всем» продажам в контракте. В результате в подобных ситуациях может быть предложено оспорить требования…

Оспаривание требований

DDD обеспечивает «конечную согласованность» между агрегатами — поскольку агрегаты определяют границу согласованности, если вы хотите определить правило, которое пересекает границу, вы должны признать, что правило может применяться не всегда.

Альтернативной реализацией было бы создание Sale собственного агрегата. В этом случае у вас не было бы ICollection<Sale> свойства на Contract — скорее у вас просто было бы ContractId свойство на Sale , и каждая продажа получала бы свой собственный глобально уникальный идентификатор.

Однако жизнеспособность этого метода зависит от того, разрешено ли изменять даты контрактов и что должно произойти, когда они это сделают … для иллюстрации:

Чтобы изменить даты продажи, вы должны использовать ContractRepository to get a Contract и SaleRepository to get a Sale и, возможно, передать контракт методу изменения даты на Sale :

 public void ChangeDate(Contract contract, DateRange dates)
{
    if (contract.Id != this.ContractId)
        throw new ArgumentException("wrong contract", "contract");

    if (!contract.AreSaleDatesValid(dates))
        throw new ArgumentException("wrong dates", "dates");

    this.Dates = dates;
}
  

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

Если нет, то этот подход прост и работоспособен и гарантирует, что вы сможете напрямую обращаться к продажам.

Однако, если они могут, то существует риск того, что даты контракта могут измениться одновременно с изменением дат продажи, и, таким образом, ваше правило будет временно нарушено.

Но именно здесь могут помочь события домена. Если ваш Sale.ChangeDate метод опубликовал событие SaleDatesChanged , и вы обрабатываете событие асинхронно в новой транзакции, тогда обработчик может проверить, действительны ли даты продажи для контракта.

Что произойдет дальше, зависит от ваших бизнес-требований — оповещение о проверке вручную или автоматическое изменение дат продажи, чтобы они соответствовали датам нового контракта?

Аналогично, Contract.ChangeDate метод будет публиковать ContractDatesChanged , и обработчик для этого проверит, что все продажи находятся в пределах дат контракта, и снова предупредит или скорректирует.

Это «конечная согласованность» из требования DDD — ваше правило о том, что все продажи должны быть в пределах дат контракта, будет выполнено … в конечном итоге.

И вот почему я сказал «оспаривание» требований — если в этих случаях действительно было бы лучше разрешить датам продаж выходить за пределы даты контракта и обрабатывать это соответствующим бизнесу способом, тогда вы поставили под сомнение свои требования и разработали более глубокое понимание предметной области.

Комментарии:

1. Я также думал об этом решении, но тогда у объекта контракта будет много функций, подобных приведенным выше. Правильно ли это с точки зрения ddd? И даже с внутренним модификатором это не мешает другим разработчикам использовать метод напрямую. Я также прочитал предложения с событиями домена, но я не нашел полной реализации.

2. Также как насчет тестирования? внутренний не помогает.

3. Расширил мой ответ, чтобы проиллюстрировать, как события домена могут помочь — если вы ослабите требования — и почему вы должны проходить через объект Contract, если у вас действительно есть требование «всегда должно быть истинным».

4. Вы можете использовать InternalsVisibleTo атрибут assembly, чтобы обеспечить видимость для тестовых проектов. Однако, что еще лучше, поскольку бизнес-правило содержится в Contract классе, вы должны протестировать его с помощью класса Contract . При тестировании проще не издеваться над объектами домена, а сосредоточиться на тестировании всего агрегата (который включает Contract и Sale ) и, таким образом, использовать метод Contract, а затем утверждать состояние продажи.

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