我可以想到不同的选项 - 根据您的具体要求 - 或多或少适合,并且也可以针对不同的用例选择不同的方法并将它们混合到您的解决方案中。
为了说明这一点,我想根据产品应用程序的操作研究不同的选项,我简单地称之为AddPriceToProduct(AddProductPriceCommand 定价命令)。它代表添加产品新价格的用例。这添加产品价格命令是一个简单的 DTO,它保存执行用例所需的所有数据。
选项(A): Inject相应的服务(例如,电子邮件服务)在将域逻辑执行到域对象的方法中时需要调用(此处AddPrice).
如果你总是选择这种方法传入一个接口(在您的域层中定义)而不是实际执行(应在基础设施层中定义)。另外,如果发生一些事情,我不会选择这种方法after您的域操作中发生了一些事情。
public void AddPriceToProduct(AddProductPriceCommand pricingCommand)
{
var product = _productRepository.findById(pricingCommand.productId);
product.AddPrice(pricingCommand.price, _emailService);
_productRepository.Update(product);
}
以及相应的AddPrice方法可能如下所示:
public void AddPrice(int price, IEmailService emailService)
{
var currentPrice = _prices.LastOrDefault();
if(price < currentPrice)
{
_prices.add(price);
// call email service with whatever parameters required
emailService.Email(this, price);
}
}
选项(B): 让应用服务(编排用例)调用相应的服务在调用应用程序用例需要执行的相应聚合(或域服务)方法之后。
如果这总是在执行特定域模型操作之后发生,那么这可能是一种简单而有效的方法。我的意思是,在您的聚合(或域服务)上调用该方法之后,在您的情况下AddPrice方法,有没有条件逻辑是否应调用其他服务(例如电子邮件)。
public void AddPriceToProduct(AddProductPriceCommand pricingCommand)
{
var product = _productRepository.findById(pricingCommand.productId);
product.AddPrice(pricingCommand.price);
_productRepository.Update(product);
// always send an email as part of the usual workflow
_emailService.Email(product, pricingCommand.price);
}
在这种情况下,我们假设正常工作流程将始终包含此附加步骤。我不认为这里务实有什么问题,只需在应用程序服务方法中调用相应的服务即可。
选项(C):类似于选项(B) but 有条件逻辑之后执行AddPrice已被调用。在这种情况下,这个逻辑可以包装成一个单独的域服务这将根据当前状态处理条件部分Product或域运算的结果(如果有的话)(AddPrice).
我们首先简单改变应用服务方法,加入一些领域知识:
public void AddPriceToProduct(AddProductPriceCommand pricingCommand)
{
var product = _productRepository.findById(pricingCommand.productId);
product.AddPrice(pricingCommand.price);
_productRepository.Update(product);
if (product.HasNewPrice())
{
_emailService.Email(product, pricingCommand.price;
}
if (product.PriceTargetAchieved())
{
_productUpdater.UpdateRatings(product, pricingCommand.price);
}
}
现在这种方法还有一些改进的空间。由于要执行的逻辑绑定到产品的 AddPrice() 方法,因此可能很容易错过需要调用的附加逻辑(在某些情况下调用电子邮件服务或更新程序服务)。当然你可以将所有服务注入添加价格()Product 实体的方法,但在这种情况下,我们想要研究将逻辑提取到域服务.
首先我们看一下新版本的应用服务方法:
public void AddPriceToProduct(AddProductPriceCommand pricingCommand)
{
var product = _productRepository.findById(pricingCommand.productId);
_productPricingService.AddPrice(product, pricingCommand.price);
_productRepository.Update(product);
}
现在让我们看一下名为的域服务的相应域服务方法,例如产品定价服务:
public void AddPrice(Product product, int price)
{
if (product.HasNewPrice())
{
_emailService.Email(product, pricingCommand.price;
}
if (product.PriceTargetAchieved())
{
_productUpdater.UpdateRatings(product, pricingCommand.price);
}
}
现在,处理产品价格更新的逻辑是在域层处理的。另外,域逻辑是更容易进行单元测试因为有更少的依赖(例如,这里不关心存储库)并且需要使用更少的测试替身(模拟)。
这是当然的仍然不是最高程度的业务逻辑封装与最低程度的依赖相结合在领域模型内部,但它至少更接近一些。
为了实现上述组合,领域事件将投入使用,但当然,这些也可能需要更多的实施工作。让我们在下一个选项中看看这个。
选项(D):从域实体引发域事件并实现相应的处理程序,这些处理程序可以是域服务甚至基础设施服务。
域事件发布者(您的域实体或域服务)和订阅者(例如电子邮件服务、产品更新程序等)之间的连接。
在这种情况下,我建议不要立即分派引发的事件,而是收集它们,并且只有在一切正常(即没有抛出异常、状态已保留等)之后才分派它们进行处理。
让我们看看添加价格()的方法Product通过使用相应的领域事件再次实体。
public void AddPrice(int price, IEmailService emailService)
{
var currentPrice = _prices.LastOrDefault();
if(price < currentPrice)
{
_prices.add(price);
RaiseEvent(
new ProductPriceUpdatedEvent(
this.Id,
price
));
}
}
The 产品价格更新事件是一个简单的类,它表示过去发生的业务事件以及该事件的订阅者所需的信息。在您的情况下,订阅者将是电子邮件服务、产品更新服务等。
考虑引发事件()方法作为一种简单方法,它将创建的事件对象添加到产品实体的集合中,以便收集从应用程序或域服务调用的一个或多个业务操作期间发生的所有事件。此事件收集功能也可以是实体基类的一部分,但这是一个实现细节。
重要的是,之后添加价格()方法执行后,应用层将确保所有收集到的事件将被分派给相应的订阅者。
这样,域模型就完全独立于基础设施服务依赖性以及事件调度代码。
The “派遣前承诺”本文中描述的方法弗拉基米尔·霍里科夫 (Vladimir Khorikov) 的博客文章 https://enterprisecraftsmanship.com/posts/domain-events-simple-reliable-solution/说明了这个想法,并且也是基于您的技术堆栈。
注意:对您的逻辑进行单元测试Product与其他解决方案相比,域实体现在非常简单,因为您没有任何依赖项,并且根本不需要模拟。测试是否在正确的操作中调用了相应的域事件也很容易,因为您只需从Product调用业务方法后的实体。
To get 回到你的问题:
如何实现这一点而不使我的域依赖于服务?
要实现这一目标,您可以查看选项(B)、(C)和(D)
或者我应该将它们传递到我的域?
这可能是一种有效的方法 - 请参阅选项(A)- 但请注意,如果在域模型类的可维护性和可测试性方面需要注入多个依赖项,那么事情会变得更加复杂。
当我在这些不同的选项之间进行选择时,我总是试图找出所执行操作的哪些部分确实属于相应的业务操作,哪些部分或多或少不相关,并且实际上并不需要使业务交易成为有效交易。
例如,如果需要由服务执行的某些操作需要发生,或者整个操作根本不应该发生(就一致性而言),那么选项 (A) - 将服务注入域模型方法 - 可能很合适。否则,我会尝试将任何后续步骤与域模型逻辑分离,在这种情况下,应考虑其他选项。