在域驱动设计中,如何确定发送电子邮件是应用程序级别还是域级别问题?

时间:2015-12-22 05:02:55

标签: php email events domain-driven-design

假设您有一个名为“Next in Line”的简单应用程序。任何等待时间较长的企业都可能希望您的软件能够为客户提供更好的线路排队体验。

利益相关者说“客户需要机票时,他们会走到我们的计算机控制台并输入他们的电子邮件地址。然后他们会收到一封电子邮件,其中包含他们的票号和唯一访问权限登录我们的应用程序更多的票务队列功能“。发送带有票号的电子邮件(以及对应用的访问权限)是业务不可或缺的一部分。

我们的命令处理程序(应用程序级别)可能如下所示:

class TakeTicketCommandHandler
{
   private $repo;

   public function __construct(TicketRepository $repo)
   {
       $this->repo = $repo;
   }

   public function handle(TakeTicketCommand $command) 
   {
      $ticket = new Ticket(new EmailValueObject($command->getEmail()));

      DB::transaction(function () {
           // Send email here from the command handler? 
           // Seems domain "leaky" because Ticket Aggregate Root is responsible for this?
           ...

           $this->repo->persist($ticket);
      }
   }
}

我们的Ticket Aggregate Root可能如下所示:

class Ticket extends AggregateRoot
{
    private $id;
    private $email;

    public function __construct(EmailValueObject $email)
    {
        $this->id = new GUID();
        $this->email = $email;

        // Maybe some more domain logic here
        ...

        // Send email here?
        // Should I have injected an interface for sending an email here?
        ...
    }        

}

问题:

  1. 如何在域驱动设计应用中实现利益相关方的电子邮件要求? (使用上面的代码演示如何做到并解释原因)。我们如何知道什么是应用程序级别要求与域级别要求?
  2. 请注意,电子邮件是在一个事务中,但是事务会在此处回滚持久性而不是像电子邮件这样的服务。如果故障单持续失败,我们可以对该电子邮件做些什么? (我们是否应该提供撤消操作,例如发送另一封电子邮件通知用户该错误?)
  3. 假设我们想让Ticket也举起像TicketQueuedEvent这样的活动。在此事件的事件处理程序中,将更新另一个Aggegrate Root。这是不好的做法/建模吗?如果应用程序级服务是为了编排域,那么通过事件聚合Root到Root的通信是一种有效的DDD方法吗?

2 个答案:

答案 0 :(得分:1)

关于3个具体问题,我将尽力回答1和2,并将3分开处理。 域的责任是解决所有业务需求。必须发送电子邮件是此业务要求的一部分,因此是域的责任。

问题是我们需要一些外部组件来执行邮件发送,我们显然不想混淆我们的域名。

首先是交易问题

正如您所提到的,它可能会回滚(或转发/提交)。 整个操作应作为一个工作单元处理。所以一切都应该失败或通过一个操作,包括电子邮件。 通常,您希望您的应用程序响应用户'请求尽快,而不是让他等待一些电子邮件服务器发送电子邮件。因此,我们倾向于编写一个条目(按照CQRS命令的思路)来指定电子邮件的所有细节,并且我们从每隔10秒钟运行一次的单独工作者作业处理该条目。

责任或逻辑应该在何处:

有几种方法可以解决这个问题。

1

我通常让域知道INotifyService的接口,它对通知客户的实现一无所知。我使用适配器模式执行实现,它可以使用通信适配器:Sms,Email等。这样当添加实现,或者客户可以选择选择特定的通信方法时,我们可以简单地使用所需的适配器,以及调用代码保持不变。 我建议您通过双重调度在您的域中使用该INotifyService,因为它可以使您的域实体更加清洁。

2

另一个选择是拥有一个Application Service层,在域操作完成后显式处理发送。 问题是您的逻辑现在是您的域逻辑的副驾驶,并且每次执行域操作,并且您需要发送电子邮件时,您必须记住调用该方法。与之前的以域为中心的解决方案类似,您需要一个工作单元,该工作单元的控制时间为#34; Atomic Unit Of Work"。 当您拥有Web应用程序时,这通常非常简单。您可以阅读有关应用服务层的更多信息: Fowler - Service Layer 希望有所帮助。

您应该使用通知机制

所以这是问题3。

这完全取决于你。 我发现,不提高事件和对它们做出反应有时很难跟踪。如果您不知道如何将系统放在一起,那么知道代码的执行顺序会变得有点困难。

不可否认,随着时间的推移,你会习惯它,并且可以使它发挥作用。

希望这一切都有意义。

答案 1 :(得分:0)

  

您展示的示例没有业务逻辑,并且在此示例中使用DDD会使您的代码过于复杂。

如何在域驱动设计应用中实现利益相关方的电子邮件要求?

  

通过发布活动并通过电子邮件订阅此电子邮件   基础设施服务。

我们如何知道什么是应用程序级别要求与域级别要求?

  

域 - 业务逻辑。   基础设施 - 抽象技术问题(数据库,电子邮件)。   应用程序 - 事务,高级日志记录和安全性。

请注意,电子邮件是在事务中,但是事务会在此处回滚持久性而不是电子邮件等服务。如果故障单持续失败,我们可以对该电子邮件做些什么? (我们是否应该提供撤消操作,例如发送另一封电子邮件通知用户错误?)

  

如果业务要求通知客户端,您可以创建emailErorEvent,如果不只是在应用程序级别上执行错误记录。

假设我们想让Ticket也引发像TicketQueuedEvent这样的事件。在此事件的事件处理程序中,将更新另一个Aggegrate Root。这是不好的做法/建模吗?如果应用程序级服务是为了协调域,那么通过事件聚合Root到Root的通信是DDD的有效方法吗?

  

发送事件是有效的方法。根之间的一致性可以   是个问题。您不应该使用的操作   聚合并且必须保持一致。

希望它有所帮助。