#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. Даты контракта действительно могут измениться в любое время, поэтому продажи должны быть изменены, чтобы агрегат контракта был согласованным.