如何"query"聚合查看命令是否可以执行
How to "query" the aggregate to see if a command can be executed
我有一个作为聚合根的电子邮件草稿,其中包含以下命令:addToRecipient
、addCcRecipient
、addBccRecipient
、updateBodyText
、uploadAttachment
、removeAttachment
并且在 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 按钮时,或者当用户从一个输入(填充接收者)到另一个(填充正文)...由您决定。
我有一个作为聚合根的电子邮件草稿,其中包含以下命令:addToRecipient
、addCcRecipient
、addBccRecipient
、updateBodyText
、uploadAttachment
、removeAttachment
并且在 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 按钮时,或者当用户从一个输入(填充接收者)到另一个(填充正文)...由您决定。