DDD:领域服务需要获取数据作为其业务规则的一部分的问题
DDD: The problem with domain services that need to fetch data as part of their business rules
假设我有一个实现以下业务规则/策略的域服务:
If the total price of all products in category 'family' exceeds 1 million, reduce the price by 50% of the family products which are older than one year.
使用基于集合的存储库
我可以简单地创建一个域服务,使用规范模式加载 'family' 类别中的所有产品,然后检查条件,如果为真,则降低价格。由于产品由基于集合的存储库自动跟踪,因此域服务根本不需要发出任何 显式 基础结构调用 – 应该如此。
使用基于持久性的存储库
我运气不好。我可能会使用存储库和规范将产品加载到我的域服务中(和以前一样),但最终,我需要发出 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
) 和策略之间的内聚性丢失,实质上允许某人将错误的产品传递到域服务中。请注意,在域服务中再次过滤产品(内存中)也无济于事,因为调用者可能只传递了所有产品的一个子集。
- 应用层不知道哪些产品发生了变化,因此被迫全部保存,这可能是一个很大的冗余工作。
问题:有没有更好的策略来应对这种情况?
我考虑过一些复杂的事情,比如调整基于持久性的存储库,使其在域服务中显示为基于集合的存储库,在内部跟踪由域服务加载的产品以保存它们再次当域服务returns.
首先,我认为为这种不属于特定聚合的逻辑选择域服务是个好主意。
而且我也同意你的观点,域服务不应该关心保存更改的聚合,将这样的东西保留在域服务之外还允许你关心管理事务 - 如果需要的话 - 应用程序。
我会务实地解决这个问题,并对您的实现做一些小改动以保持简单:
// 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 的实现我只能假设它会调用产品聚合上的相应方法让他们应用折扣价格。例如。通过在 Product 聚合上使用 ApplyFamilyDiscount() 之类的方法。考虑到这一点,考虑到遍历所有产品并调用折扣方法将只是聚合之外的逻辑,具有从存储库获取所有产品的步骤,调用 ApplyFamilyDiscount() 方法在所有产品上保存所有更改的产品确实可以只驻留在应用程序层。
考虑到域模型纯度与域模型完整性(参见下面关于DDD trilemma的讨论)这将在纯度的方向,但如果循环遍历产品并调用 ApplyFamilyDiscount() 就是它所做的一切,也会使域服务受到质疑(考虑到通过存储库获取相应产品是在应用程序层中预先完成的,并且产品列表已经通过域服务)。同样,没有教条主义的方法,了解不同的选择及其权衡是相当重要的。例如,还可以考虑通过在询问价格时应用所有适用的可能折扣,让产品始终按需计算当前价格。但同样,这种解决方案是否可行取决于具体要求。
假设我有一个实现以下业务规则/策略的域服务:
If the total price of all products in category 'family' exceeds 1 million, reduce the price by 50% of the family products which are older than one year.
使用基于集合的存储库
我可以简单地创建一个域服务,使用规范模式加载 'family' 类别中的所有产品,然后检查条件,如果为真,则降低价格。由于产品由基于集合的存储库自动跟踪,因此域服务根本不需要发出任何 显式 基础结构调用 – 应该如此。
使用基于持久性的存储库
我运气不好。我可能会使用存储库和规范将产品加载到我的域服务中(和以前一样),但最终,我需要发出 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
) 和策略之间的内聚性丢失,实质上允许某人将错误的产品传递到域服务中。请注意,在域服务中再次过滤产品(内存中)也无济于事,因为调用者可能只传递了所有产品的一个子集。 - 应用层不知道哪些产品发生了变化,因此被迫全部保存,这可能是一个很大的冗余工作。
问题:有没有更好的策略来应对这种情况?
我考虑过一些复杂的事情,比如调整基于持久性的存储库,使其在域服务中显示为基于集合的存储库,在内部跟踪由域服务加载的产品以保存它们再次当域服务returns.
首先,我认为为这种不属于特定聚合的逻辑选择域服务是个好主意。
而且我也同意你的观点,域服务不应该关心保存更改的聚合,将这样的东西保留在域服务之外还允许你关心管理事务 - 如果需要的话 - 应用程序。
我会务实地解决这个问题,并对您的实现做一些小改动以保持简单:
// 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 的实现我只能假设它会调用产品聚合上的相应方法让他们应用折扣价格。例如。通过在 Product 聚合上使用 ApplyFamilyDiscount() 之类的方法。考虑到这一点,考虑到遍历所有产品并调用折扣方法将只是聚合之外的逻辑,具有从存储库获取所有产品的步骤,调用 ApplyFamilyDiscount() 方法在所有产品上保存所有更改的产品确实可以只驻留在应用程序层。
考虑到域模型纯度与域模型完整性(参见下面关于DDD trilemma的讨论)这将在纯度的方向,但如果循环遍历产品并调用 ApplyFamilyDiscount() 就是它所做的一切,也会使域服务受到质疑(考虑到通过存储库获取相应产品是在应用程序层中预先完成的,并且产品列表已经通过域服务)。同样,没有教条主义的方法,了解不同的选择及其权衡是相当重要的。例如,还可以考虑通过在询问价格时应用所有适用的可能折扣,让产品始终按需计算当前价格。但同样,这种解决方案是否可行取决于具体要求。