DDD 域服务

DDD Domain services

我有一个发票聚合根,有时可以将其发送到会计外部 Web 服务,并通过保留从该服务获得的一些 ID/number 来标记为已发送。

在 DDD 中哪种方法是正确的?

这是我的想法:

第一种方法:

有一个invoice AggregateRoot函数SendToAccounting,并注入域服务/接口,将发票发送给会计,并在会计软件中检索一些"id/code",并设置AccountingSoftwareId 属性

Invoice.SendToAccounting(IInvoiceDomain service)
{
     var accountingSoftwareID = service.getAccountingSoftwareId(this);
     this.AccountingSoftwareId = accountingSoftwareId;
}

///Implementation in the application service
    var invoice = _invoiceRepository.GetInvoiceById(id);
    invoice.SendToAccounting(someDomainService);
    _invoiceRepository.Update(invoice);
    _unitOfWork.Save();

第二种方法:

与第一种方法类似,但域服务应负责像这样持久化:

var invoice = _invoiceRepository.GetInvoiceById(id);
///unit of work save will be called inside this function
invoice.SendToAccounting(someDomainService);

第三种方法:

域服务将完全负责封装此行为

///Code inside domain service
public void SendInvoiceToAccounting(int invoiceId)
{
    var invoice =  _invoiceRepository.GetInvoiceById(invoiceId);
    string invoiceAccountingId = _accountingService.GetAccountingSoftwareId(invoice);
    invoice.SetAsSentToAccounting(invoiceAccountingId);
    _invoiceRepository.Update(invoice);
    _unitOfWork.Save();
}

我的第一个想法是“开发票不是会计的一部分吗?” :)

选项 1 是我过去倾向于在我的域对象具有行为的地方使用的选项。

我不喜欢选项 2,因为发票需要对存储库的私有引用。

一个更普遍的观察是,这里的域中似乎没有太多行为——它似乎只是在设置一个 id。选项 3 似乎抓住了这一点。我想知道应用程序服务是否足够并仅协调以下内容

  1. 加载发票
  2. 获取 AccountingId
  3. 保存在发票上

这几乎就是上面的选项 3。不过,我很想传入存储库和服务,但这实际上只是一种更实用的样式——上面的样式也适用于私有字段。

Which is the correct way to do it in DDD?

您的第一个方法最接近。您域服务上的签名应该接受状态作为参数,而不是聚合根本身。

Invoice.SendToAccounting(IInvoiceDomain service)
{
    var accountingSoftwareID = service.getAccountingSoftwareId(this.Id, ...);
    this.AccountingSoftwareId = accountingSoftwareId;
}

传递的所有参数都应该是值类型——域服务不应该能够通过操纵它的参数副本来改变聚合的状态,而且它当然不应该能够运行 聚合上的其他命令。

在代码审查中,我会拒绝你提供的第二种方法;从领域模型的角度来看,领域服务接口应该只提供查询,而不是命令(在 CQS 意义上)。

在代码审查中,我会完全拒绝第三种方法——聚合上的 setter 是一种代码味道;重点是用更新规则封装状态。

但是

该设计有些令人担忧,因为您在同一事务中的两个不同位置进行写入。在快乐的道路上,这没什么大不了的,但是如果会计服务上的命令运行成功,但是更新的发票保存失败怎么办?

假设分布式事务没有吸引力,您可能想回顾一下 Udi Dahan 对 reliable messaging 的看法。

会计 BC 应始终return 给定发票 ID 的相同 accountingSoftwareId。

如果在第一轮中,在会计BC上调用但发票更新失败,则您在帐户BC中的状态为t1,在发票BC中的状态为t0。当您重试该命令时,它将执行相同的调用和 return 相同的 id,如果更新成功,您将在每个 BC 中处于 t1 状态。在最坏的情况下,即使必须手动解析命令,对于给定的发票 ID,生成的帐户 ID 也将始终相同。

所以要解决特定发票的会计 ID,您可以直接询问会计 BC。