DDD:需要获取数据作为其业务规则一部分的领域服务的问题

dom*_*min 2 domain-driven-design repository-design domainservices

假设我有一个实现以下业务规则/策略的域服务:

\n
\n

如果“家庭”类目中所有产品的总价超过100万,则对超过一年的家庭产品降价50%。

\n
\n

使用基于集合的存储库

\n

我可以简单地创建一个域服务,使用规范模式加载“系列”类别中的所有产品,然后检查条件,如果为真,则降低价格。由于产品是由基于集合的存储库自动跟踪的,因此域服务不需要像应该那样发出任何显式的基础结构调用 \xe2\x80\x93 。

\n

使用基于持久性的存储库

\n

我运气不好。我可能会摆脱使用存储库和规范在我的域服务中加载产品的情况(像以前一样),但最终,我需要发出Save不属于域层的调用。

\n

我可以在应用程序层加载产品,然后将它们传递给域服务,最后再次将它们保存在应用程序层,如下所示:

\n
// Somewhere in the application layer:\npublic void ApplyProductPriceReductionPolicy()\n{\n  // make sure everything is in one transaction\n  using (var uow = this.unitOfWorkProvider.Provide())\n  {\n    // fetching\n    var spec = new FamilyProductsSpecification();\n    var familyProducts = this.productRepository.findBySpecification(spec);\n\n    // business logic (domain service call)\n    this.familyPriceReductionPolicy.Apply(familyProducts);\n\n    // persisting\n    foreach (var familyProduct in familyProducts)\n    {\n      this.productRepository.Save(familyProduct);\n    }\n\n    uow.Complete();\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

但是,我发现这段代码存在以下问题:

\n
    \n
  • 加载正确的产品现在是应用程序层的一部分,因此,如果我需要在其他用例中再次应用相同的策略,我需要重复一遍。
  • \n
  • 规范 ( FamilyProductsSpecification) 和策略之间的内聚性丢失,本质上允许某人将错误的产品传递到域服务中。请注意,在域服务中再次过滤产品(内存中)也没有帮助,因为调用者可能只传递了所有产品的子集。
  • \n
  • 应用层不知道哪些产品发生了变化,因此被迫保存所有产品,这可能是很多多余的工作。
  • \n
\n

问题:有没有更好的策略来处理这种情况?

\n

我考虑了一些复杂的事情,比如调整基于持久性的存储库,使其在域服务中显示为基于集合的存储库,在内部跟踪域服务加载的产品,以便在域服务启动时再次保存它们返回。

\n

afh*_*afh 6

首先,我认为为这种逻辑(不属于某个特定聚合)选择域服务是一个好主意。

我也同意您的观点,域服务不应该关心保存更改的聚合,将此类内容保留在域服务之外还允许您关心应用程序的事务管理(如果需要)。

我会务实地对待这个问题,并对您的实现进行一些小更改以保持简单:

// 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();
  }
}
Run Code Online (Sandbox Code Playgroud)

在产品域服务中的实现:

// 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;
}
Run Code Online (Sandbox Code Playgroud)

这样,检查一年以上的所有系列产品然后应用当前折扣(50%)的整个业务逻辑就封装在域服务中。然后,应用程序层再次只负责编排以正确的顺序调用正确的逻辑。当然,您希望通过提供参数来使域服务方法变得通用的命名和通用程度可能需要调整,但如果无论如何只有一个特定的业务需求,我通常会尝试不要使任何内容变得太通用。因此,如果这是当前的系列产品折扣,我就已经知道我到底需要在哪里更改实现 - 仅在域服务方法中。

老实说,如果应用程序方法没有变得更复杂并且您没有不同的分支(例如 if 条件),我通常会像您最初提出的那样开始,因为应用程序层方法也只是简单地调用域服务(在您的情况是存储库)具有相应的参数,并且其中没有条件逻辑。如果它变得更复杂,我会将其重构为域服务方法,例如我建议的方式。

注意:由于我不知道 FamilyPriceRedcutionPolicy 的实现,我只能假设它将调用产品聚合上的相应方法,让它们应用价格折扣。例如,通过在 Product 聚合上使用诸如ApplyFamilyDiscount()之类的方法。考虑到这一点,考虑到循环所有产品并调用折扣方法将只是聚合之外的逻辑,具有从存储库获取所有产品、对所有产品调用 ApplyFamilyDiscount() 方法保存所有更改的产品的步骤确实可以只驻留在应用程序层。

就考虑领域模型纯度与领域模型完整性而言(请参阅下面有关DDD 三难困境的讨论),这将使实现再次朝着纯度方向移动一点,但如果循环遍历产品并调用ApplyFamilyDiscount() 就是它所做的全部(考虑到通过存储库获取相应产品是预先在应用程序层完成的,并且产品列表已经传递到域服务)。再说一次,不存在教条的方法,了解不同的选择及其权衡相当重要。例如,人们还可以考虑让产品始终通过在询问价格时应用所有适用的可能折扣来按需计算当前价格。但同样,这种解决方案是否可行取决于具体要求。

  • 这是领域 [纯度、完整性和性能](https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/) 之间的经典 DDD 三难困境。 (3认同)