在域驱动设计中,如何确定发送电子邮件是应用程序级别还是域级别问题?
In Domain-Driven Design, how to determine if sending an email is an application level or domain level concern?
假设您有一个名为 "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);
}
}
}
我们的工单聚合根可能如下所示:
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?
...
}
}
问题:
- 利益相关者的电子邮件要求应该如何在域驱动设计应用程序中实现? (使用上面的代码来演示您将如何做并解释原因)。我们如何知道什么时候是应用程序级要求与域级要求?
- 请注意电子邮件在事务中,但是事务回滚是为了持久性,而不是像电子邮件这样的服务。如果票据持久化失败,我们可以如何处理电子邮件? (我们是否应该提供撤消操作,例如发送另一封电子邮件通知用户错误?)
- 假设我们想让 Ticket 也引发类似
TicketQueuedEvent
的事件。在此事件的事件处理程序中,更新了另一个聚合根。这不好吗practice/modelling?如果应用程序级服务旨在编排域,那么通过事件聚合根到根通信是否是 DDD 的有效方法?
至于3个具体问题,我会尽力回答1和2,而3会分开处理。
Domain 有责任解决所有的业务需求。必须发送电子邮件是此业务要求的一部分,因此由域负责。
问题是我们需要一些外部组件来执行邮件发送,我们显然不想混淆我们的域。
一、交易问题:
正如您提到的,它可以回滚(或转发/提交)。
整个操作应作为一个工作单元来处理。所以一切都应该作为一个操作失败或通过,包括电子邮件。
通常,您希望您的应用程序尽快响应用户的请求,而不是让他在某个电子邮件服务器发送电子邮件时等待。因此,我们倾向于编写一个条目(按照 CQRS 命令的思路)来指定电子邮件的所有详细信息,并且我们从每 10 秒左右运行一次的单独工作作业中处理该条目。
责任或逻辑应在何处:
有几种方法可以解决这个问题。
1.
我通常让域知道 INotifyService
的接口,它对通知客户的实现一无所知。我使用适配器模式来实现,它可以采用通信适配器,比如:短信、电子邮件等。这样,当添加实现时,或者客户可以选择加入特定的通信方法时,我们可以简单地使用所需的适配器,并且调用代码保持不变。
我建议通过双重调度在您的域中使用该 INotifyService,因为它可以使您的域实体更清洁。
2.
另一种选择是让应用服务层在域操作完成后显式处理发送。
问题是您的逻辑现在是您的域逻辑的附属工具,每次执行该域操作并且您需要发送电子邮件时,您必须记住调用该方法。与之前的以领域为中心的解决方案类似,您需要一个工作单元,该工作单元在 "Atomic Unit Of Work" 的持续时间内受到控制。
当您有一个 Web 应用程序时,这通常很容易。您可以阅读有关应用程序服务层的更多信息:
Fowler - Service Layer
希望能有所帮助。
你应该使用通知机制
所以这是问题 3。
这完全取决于您。
我发现,不引发事件并对其做出反应有时很难跟踪。如果你不知道系统是如何组合在一起的,就很难知道代码的执行顺序。
不可否认,随着时间的推移您会习惯它,并且它可以发挥作用。
希望这一切都有意义。
Example which you show don't have business logic and following DDD in this example will overcomplicate you code.
如何在 Domain-Driven 设计应用程序中实现来自利益相关者的电子邮件要求?
By publishing event and subscribe to this email by email
infrastructure service.
我们如何知道什么时候是应用程序级要求与域级要求?
Domain - business logic.
Infrastructure - abstract technical concerns (db, email).
Application - transactions, high-level logging and security.
注意电子邮件是在一个事务中,但是这里的事务回滚是为了持久化,而不是像电子邮件这样的服务。如果票据持久化失败,我们可以如何处理电子邮件? (我们是否应该提供撤消操作,例如发送另一封电子邮件通知用户错误?)
If business ask to notify client you can create emailErorEvent if not just do error logging on application level.
假设我们想让 Ticket 也引发类似 TicketQueuedEvent 的事件。在此事件的事件处理程序中,更新了另一个聚合根。这很糟糕 practice/modelling 吗?如果应用程序级服务旨在编排域,那么通过事件聚合根到根通信是否是一种有效的 DDD 方法?
sending events it is valid approach. Consistency between roots can
be a problem. You should not have operations that use
aggregates and have to be consistent.
希望对您有所帮助。
假设您有一个名为 "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);
}
}
}
我们的工单聚合根可能如下所示:
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?
...
}
}
问题:
- 利益相关者的电子邮件要求应该如何在域驱动设计应用程序中实现? (使用上面的代码来演示您将如何做并解释原因)。我们如何知道什么时候是应用程序级要求与域级要求?
- 请注意电子邮件在事务中,但是事务回滚是为了持久性,而不是像电子邮件这样的服务。如果票据持久化失败,我们可以如何处理电子邮件? (我们是否应该提供撤消操作,例如发送另一封电子邮件通知用户错误?)
- 假设我们想让 Ticket 也引发类似
TicketQueuedEvent
的事件。在此事件的事件处理程序中,更新了另一个聚合根。这不好吗practice/modelling?如果应用程序级服务旨在编排域,那么通过事件聚合根到根通信是否是 DDD 的有效方法?
至于3个具体问题,我会尽力回答1和2,而3会分开处理。 Domain 有责任解决所有的业务需求。必须发送电子邮件是此业务要求的一部分,因此由域负责。
问题是我们需要一些外部组件来执行邮件发送,我们显然不想混淆我们的域。
一、交易问题:
正如您提到的,它可以回滚(或转发/提交)。 整个操作应作为一个工作单元来处理。所以一切都应该作为一个操作失败或通过,包括电子邮件。 通常,您希望您的应用程序尽快响应用户的请求,而不是让他在某个电子邮件服务器发送电子邮件时等待。因此,我们倾向于编写一个条目(按照 CQRS 命令的思路)来指定电子邮件的所有详细信息,并且我们从每 10 秒左右运行一次的单独工作作业中处理该条目。
责任或逻辑应在何处:
有几种方法可以解决这个问题。
1.
我通常让域知道 INotifyService
的接口,它对通知客户的实现一无所知。我使用适配器模式来实现,它可以采用通信适配器,比如:短信、电子邮件等。这样,当添加实现时,或者客户可以选择加入特定的通信方法时,我们可以简单地使用所需的适配器,并且调用代码保持不变。
我建议通过双重调度在您的域中使用该 INotifyService,因为它可以使您的域实体更清洁。
2.
另一种选择是让应用服务层在域操作完成后显式处理发送。 问题是您的逻辑现在是您的域逻辑的附属工具,每次执行该域操作并且您需要发送电子邮件时,您必须记住调用该方法。与之前的以领域为中心的解决方案类似,您需要一个工作单元,该工作单元在 "Atomic Unit Of Work" 的持续时间内受到控制。 当您有一个 Web 应用程序时,这通常很容易。您可以阅读有关应用程序服务层的更多信息: Fowler - Service Layer 希望能有所帮助。
你应该使用通知机制
所以这是问题 3。
这完全取决于您。 我发现,不引发事件并对其做出反应有时很难跟踪。如果你不知道系统是如何组合在一起的,就很难知道代码的执行顺序。
不可否认,随着时间的推移您会习惯它,并且它可以发挥作用。
希望这一切都有意义。
Example which you show don't have business logic and following DDD in this example will overcomplicate you code.
如何在 Domain-Driven 设计应用程序中实现来自利益相关者的电子邮件要求?
By publishing event and subscribe to this email by email infrastructure service.
我们如何知道什么时候是应用程序级要求与域级要求?
Domain - business logic. Infrastructure - abstract technical concerns (db, email). Application - transactions, high-level logging and security.
注意电子邮件是在一个事务中,但是这里的事务回滚是为了持久化,而不是像电子邮件这样的服务。如果票据持久化失败,我们可以如何处理电子邮件? (我们是否应该提供撤消操作,例如发送另一封电子邮件通知用户错误?)
If business ask to notify client you can create emailErorEvent if not just do error logging on application level.
假设我们想让 Ticket 也引发类似 TicketQueuedEvent 的事件。在此事件的事件处理程序中,更新了另一个聚合根。这很糟糕 practice/modelling 吗?如果应用程序级服务旨在编排域,那么通过事件聚合根到根通信是否是一种有效的 DDD 方法?
sending events it is valid approach. Consistency between roots can be a problem. You should not have operations that use aggregates and have to be consistent.
希望对您有所帮助。