【问题标题】:DDD Domain servicesDDD 域服务
【发布时间】:2016-11-15 07:06:13
【问题描述】:

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

在 DDD 中正确的做法是什么?

这是我的想法:

第一种方法:

有一个带有SendToAccounting功能的发票AggregateRoot,并注入域服务/接口,它将发票发送到会计,并在会计软件中检索一些“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();
}

【问题讨论】:

  • 我不清楚发送给会计的过程涉及什么 - 这是什么?
  • 调用外部服务失败时如何处理?
  • 发票汇总中使用的accountingId是什么?

标签: domain-driven-design ddd-service


【解决方案1】:

会计 BC 应始终为给定的 invoiceId 返回相同的 accountingSoftwareId。

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

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

【讨论】:

    【解决方案2】:

    在 DDD 中正确的做法是什么?

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

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

    所有传递的参数都应该是值类型——域服务不应该能够通过操作参数的副本来改变聚合的状态,当然也不应该能够在聚合。

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

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

    但是

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

    假设分布式事务没有吸引力,您可能需要查看 Udi Dahan 对 reliable messaging 的评价。

    【讨论】:

      【解决方案3】:

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

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

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

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

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

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

      【讨论】:

        猜你喜欢
        • 2019-08-23
        • 2014-12-02
        • 2013-06-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-03
        • 2021-01-08
        • 2013-07-18
        相关资源
        最近更新 更多