如何"query"聚合查看命令是否可以执行

How to "query" the aggregate to see if a command can be executed

我有一个作为聚合根的电子邮件草稿,其中包含以下命令:addToRecipientaddCcRecipientaddBccRecipientupdateBodyTextuploadAttachmentremoveAttachment 并且在 UI 中,如果草稿尚未准备好发送,我想禁用发送按钮(即至少有收件人并且正文有文本)。我知道我不能查询聚合,但它是唯一可以告诉我可以或不能发送电子邮件的聚合。

如果我要应用我对事件溯源和 CQRS 的了解,那么聚合将发出一个 EmailIsReadyToBeSent 事件,我的 UserEmailDrafts 读取模型将选择该事件并更新 UI 不知何故,然后,我将不得不在每个命令后检查并发送一个取消事件,即 EmailIsNotReadyToBeSent

这个感觉很复杂,你怎么看?

除非有收件人和正文,否则无法发送电子邮件这一事实与应用逻辑接壤,因为归根结底,这更多的是在表单上填写字段,而不是复杂的域不变量。

与其依赖于每次屏幕上发生变化时查询读取模型的完整跨层往返,我会在 UI 中注入一些这些基本规则的知识,以便按钮立即指定收件人和正文时重新启用。

实际上,当您看到客户端逻辑在表单上执行必填字段验证时,您并不感到震惊。这是一个完全有效且可以接受的权衡,因为逻辑简单且通用。

请注意,这并不妨碍您将这些规则也纳入聚合中,拒绝任何不满足这些规则的命令。

我将尝试用 Specification 模式的示例扩展@plalx 给出的答案。

为了示例,我将使用 this ddd library 中的一些 类。特别是定义接口以使用规范模式的那些(由@martinezdelariva 提供)

首先,让我们忘掉 UI 并将重点放在您必须满足的域不变量上。所以你说为了发送电子邮件,电子邮件需要:

  • 不包含禁用关键字。
  • 包含至少一个收件人和正文内容。
  • 是唯一的,这意味着类似的电子邮件尚未发送。

现在让我们看一下应用程序服务(用例),以便在深入了解细节之前了解全局:

class SendEmailService implements ApplicationService
{
    /**
     * @var EmailRepository
     */
    private $emailRepository;

    /**
     * @var CanSendEmailSpecificationFactory
     */
    private $canSendEmailSpecFactory;

    /**
     * @var EmailMessagingService
     */
    private $emailMessagingService;

    /**
     * @param EmailRepository $emailRepository
     * @param CanSendEmailSpecificationFactory $canSendEmailSpecFactory
     */
    public function __construct(
        EmailRepository $emailRepository,
        CanSendEmailSpecificationFactory $canSendEmailSpecFactory,
        EmailMessagingService $emailMessagingService
    ) {
        $this->emailRepository = $emailRepository;
        $this->canSendEmailSpecFactory = $canSendEmailSpecFactory;
        $this->emailMessagingService = $emailMessagingService;
    }

    /**
     * @param $request
     *
     * @return mixed
     */
    public function execute($request = null)
    {
        $email = $this->emailRepository->findOfId(new EmailId($request->emailId()));
        $canSendEmailSpec = $this->canSendEmailSpecFactory->create();

        if ($email->canBeSent($canSendEmailSpec)) {
            $this->emailMessagingService->send($email);
        }
    }
}

我们从repo 中获取邮件,检查是否可以发送并发送。那么让我们看看聚合根(电子邮件)如何使用不变量,这里是 canBeSent 方法:

/**
 * @param CanSendEmailSpecification $specification
 *
 * @return bool
 */
public function canBeSent(CanSendEmailSpecification $specification)
{
    return $specification->isSatisfiedBy($this);
}

到目前为止一切顺利,现在让我们看看复合 CanSendEmailSpecification 来满足我们的不变量是多么容易:

class CanSendEmailSpecification extends AbstractSpecification
{
    /**
     * @var Specification
     */
    private $compoundSpec;

    /**
     * @param EmailFullyFilledSpecification               $emailFullyFilledSpecification
     * @param SameEmailTypeAlreadySentSpecification       $sameEmailTypeAlreadySentSpec
     * @param ForbiddenKeywordsInBodyContentSpecification $forbiddenKeywordsInBodyContentSpec
     */
    public function __construct(
        EmailFullyFilledSpecification $emailFullyFilledSpecification,
        SameEmailTypeAlreadySentSpecification $sameEmailTypeAlreadySentSpec,
        ForbiddenKeywordsInBodyContentSpecification $forbiddenKeywordsInBodyContentSpec
    ) {
        $this->compoundSpec = $emailFullyFilledSpecification
            ->andSpecification($sameEmailTypeAlreadySentSpec->not())
            ->andSpecification($forbiddenKeywordsInBodyContentSpec->not());
    }

    /**
     * @param mixed $object
     *
     * @return bool
     */
    public function isSatisfiedBy($object)
    {
        return $this->compoundSpec->isSatisfiedBy($object);
    }
}

如您所见,我们在这里说,为了发送电子邮件,我们必须满足:

  • 邮件已填满(此处可查看正文内容是否为空且至少有一位收件人)
  • 并且尚未发送相同类型的电子邮件。
  • 并且正文内容中没有禁止使用的词。

在下面找到前两个规范的实现:

class EmailFullyFilledSpecification extends AbstractSpecification
{
    /**
     * @param EmailFake $email
     *
     * @return bool
     */
    public function isSatisfiedBy($email)
    {
        return $email->hasRecipient() && !empty($email->bodyContent());
    }
}
class SameEmailTypeAlreadySentSpecification extends AbstractSpecification
{
    /**
     * @var EmailRepository
     */
    private $emailRepository;

    /**
     * @param EmailRepository $emailRepository
     */
    public function __construct(EmailRepository $emailRepository)
    {
        $this->emailRepository = $emailRepository;
    }

    /**
     * @param EmailFake $email
     *
     * @return bool
     */
    public function isSatisfiedBy($email)
    {
        $result = $this->emailRepository->findAllOfType($email->type());

        return count($result) > 0 ? true : false;
    }
}

感谢规范模式,您现在可以管理老板要求您添加的不变量,而无需修改现有代码。您也可以非常轻松地为每个规范创建单元测试。

另一方面,您可以使 UI 尽可能复杂,让用户知道电子邮件已准备好发送。我将创建另一个用例 ValidateEmailService,当用户单击 validate 按钮时,或者当用户从一个输入(填充接收者)到另一个(填充正文)...由您决定。