DDD: проблема с доменными службами, которым необходимо получать данные как часть их бизнес-правил.

Предположим, у меня есть служба домена, которая реализует следующее бизнес-правило / политику:

Если общая цена всех продуктов в категории «семейство» превышает 1 миллион, снизьте цену на 50% для семейных продуктов старше одного года.

Использование репозиториев на основе коллекций

Я могу просто создать службу домена, которая загружает все продукты из категории «семейство», используя шаблон спецификации, затем проверять условие и, если оно истинно, снижать цены. Поскольку продукты автоматически отслеживаются репозиторием на основе коллекции, от службы домена вообще не требуется выполнять какие-либо явные вызовы инфраструктуры - как и должно быть.

Использование репозиториев на основе сохраняемости

Мне не повезло. Я мог бы обойтись без использования репозитория и спецификации для загрузки продуктов в мою доменную службу (как и раньше), но в конечном итоге мне нужно сделать Save вызовы, которые не принадлежат уровню домена.

Я мог бы загрузить продукты на уровне приложения, затем передать их службе домена и, наконец, снова сохранить их на уровне приложения, например:

// Somewhere in the application layer:
public void ApplyProductPriceReductionPolicy()
{
  // make sure everything is in one transaction
  using (var uow = this.unitOfWorkProvider.Provide())
  {
    // fetching
    var spec = new FamilyProductsSpecification();
    var familyProducts = this.productRepository.findBySpecification(spec);

    // business logic (domain service call)
    this.familyPriceReductionPolicy.Apply(familyProducts);

    // persisting
    foreach (var familyProduct in familyProducts)
    {
      this.productRepository.Save(familyProduct);
    }

    uow.Complete();
  }
}

Однако я вижу следующие проблемы с этим кодом:

  • Загрузка правильных продуктов теперь является частью уровня приложения, поэтому, если мне нужно снова применить ту же политику в каком-то другом случае использования, мне нужно будет повторить себя.
  • Связь между спецификацией (FamilyProductsSpecification) и политикой теряется, что позволяет кому-то передавать неправильные продукты в службу домена. Обратите внимание, что повторная фильтрация продуктов (в памяти) в службе домена также не помогает, так как вызывающий мог передать только подмножество всех продуктов.
  • Уровень приложения не знает, какие продукты были изменены, и поэтому вынужден сохранять их все, что может быть большой избыточной работой.

Вопрос: есть ли лучшая стратегия в этой ситуации?

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


person domin    schedule 25.04.2021    source источник


Ответы (1)


Прежде всего, я думаю, что выбор службы домена для такого рода логики, которая не принадлежит одному конкретному агрегату, является хорошей идеей.

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

Я бы отнесся к этой проблеме прагматично и немного изменил вашу реализацию, чтобы она была простой:

// Somewhere in the application layer:
public void ApplyProductFamilyDiscount()
{
  // make sure everything is in one transaction
  using (var uow = this.unitOfWorkProvider.Provide())
  {

    var familyProducts = this.productService.ApplyFamilyDiscount();

    // persisting
    foreach (var familyProduct in familyProducts)
    {
      this.productRepository.Save(familyProduct);
    }

    uow.Complete();
  }
}

Реализация в сервисе предметной области:

// some method of the product domain service
public IEnumerable<Product> ApplyFamilyDiscount()
{
    var spec = new FamilyProductsSpecification();
    var familyProducts = this.productRepository.findBySpecification(spec);

    this.familyPriceReductionPolicy.Apply(familyProducts);

    return familyProducts;
}

При этом вся бизнес-логика прохождения всех продуктов семейства старше одного года с последующим применением текущей скидки (50 процентов) инкапсулируется внутри службы домена. Прикладной уровень снова отвечает только за то, чтобы правильная логика вызывалась в правильном порядке. Именование и то, насколько универсальными вы хотите сделать методы службы домена, предоставляя параметры, конечно, могут нуждаться в настройке, но я обычно стараюсь не делать ничего слишком общего, если в любом случае есть только одно конкретное бизнес-требование. Так что, если это текущая скидка на семейный продукт, я бы уже знал, где именно мне нужно изменить реализацию - только в методе службы домена.

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

Примечание. Поскольку я не знаю реализации FamilyPriceRedcutionPolicy, я могу только предположить, что она вызовет соответствующий метод для агрегатов продуктов, чтобы позволить им применить скидку на цену. Например. используя такой метод, как ApplyFamilyDiscount () для агрегата Product. Имея это в виду, учитывая, что цикл по всем продуктам и вызов метода скидки будет только логикой за пределами агрегата, имея этапы получения всех продуктов из репозитория, вызывая метод ApplyFamilyDiscount () для все продукты и сохранение всех измененных продуктов могут действительно просто находиться на уровне приложения.

С точки зрения рассмотрения чистоты модели предметной области и полноты модели предметной области (см. Обсуждение ниже), это снова немного сдвинет Реализацию в сторону чистоты, но также сделает службу предметной области сомнительной при циклическом переходе по продуктам. и вызов ApplyFamilyDiscount () - это все, что он делает (учитывая, что получение соответствующих продуктов через репозиторий выполняется заранее на уровне приложения, а список продуктов уже передан в службу домена). Итак, опять же, нет догматического подхода, и довольно важно знать различные варианты и их компромиссы. Например, можно также подумать о том, чтобы позволить продукту всегда рассчитывать текущую цену по запросу, применяя все применимые возможные скидки при запросе цены. Но опять же, осуществимость такого решения зависит от конкретных требований.

person afh    schedule 26.04.2021
comment
Хотя возвращение измененных объектов из доменных служб как бы незаметно загрязняет дизайн API уровня домена проблемами инфраструктуры, я мог бы жить с этим правилом, особенно если кто-то найдет подходящие доменные имена для этих возвращаемых типов (как вы это сделали с familyProducts) . Я не совсем понимаю твой последний абзац. Вы предлагаете, чтобы отсутствие ветвления в бизнес-логике узаконивало размещение ее непосредственно на уровне приложения? Разве вызов чего-либо в правильном порядке не является важной частью бизнес-логики? - person domin; 26.04.2021
comment
Спасибо за заметку, это была ошибка в моем посте, я ее исправил. Я имел в виду способ применения. Я имел в виду, что если метод приложения (или вариант использования) будет простым, это может быть прагматическим решением не создавать сразу отдельную службу домена и реорганизовать код в службу домена, если его нужно где-то повторно использовать как или если это станет еще сложнее. Конечно, это связано не только с наличием ветвей в коде, это больше похоже на пример, потому что условный код часто может указывать на более сложную логику. - person afh; 26.04.2021
comment
В конце концов, это сочетание лучших практик, решение основано на конкретной ситуации, а иногда и на личных предпочтениях. Как было сказано, я лично уже видел шаги по дисконтированию в отдельной службе домена, но не имел бы догматических возражений против предложенного вами кода, если это все еще единственное место, где это вызывается. Но помимо того, что код инкапсулирован в другое отдельное место (например, метод службы домена), придающий этому методу службы домена имя, имеющее отношение к бизнесу, также является хорошим плюсом извлечения с точки зрения удобочитаемости. - person afh; 26.04.2021
comment
Понятно, спасибо за разъяснения. Я все еще немного не понимаю, что такое вариант использования термина. Вероятно, существует различие между вариантами использования приложений и бизнес-вариантами использования. Только первое должно находиться непосредственно на уровне приложения, тогда как второе предпочтительно всегда должно быть в какой-либо доменной службе (без каких-либо прагматических решений, направленных против этого идеального мира), правильно? - person domin; 26.04.2021
comment
Это классическая трилемма DDD между чистотой, полнотой и производительностью домена. - person plalx; 26.04.2021
comment
@plalx, ​​замечательная статья и блог! Это наиболее подробное и по существу обсуждение нескольких проблем, с которыми я столкнулся с момента изучения DDD, с которыми я когда-либо сталкивался. Что мне интересно, так это предпочтение чистоты модели предметной области над полнотой модели предметной области. Решение @afh делает наоборот. Скажем, мы не называем репозиторий productRepository, а вместо этого просто products и называем тип AggregateCollection<Product>. Конечно, вызов не является ссылочно прозрачным, но, по крайней мере, именование скрывает его внепроцессный характер - это именно то, для чего нужны репозитории на основе коллекций. - person domin; 26.04.2021
comment
Я добавил несколько примечаний к своему ответу с дополнительными мыслями, которые пришли мне в голову после того, как я принял во внимание совет Владимира Хорикова. Хотя я думаю, что это хорошая идея, держать вне процесса зависимости вне доменных служб, я все же думаю, что могут быть ситуации, когда разрешение доменным службам, имеющим доступ к репозиториям, может быть допустимым вариантом, особенно если тестируемость бизнес-логики не является допустимой. отрицательно повлиял. - person afh; 28.04.2021
comment
Большой! Я согласен. ИМО, крайне важно загрузить правильные данные, прежде чем выполнять какую-либо логику. Поэтому я предпочитаю полноту чистоте, так как тестирование службы является прямым путем предоставления коллекции тестовых продуктов, созданной в памяти. - person domin; 28.04.2021