我有一个电子邮件草稿作为聚合根,包含以下命令:addToRecipient
,addCcRecipient
,addBccRecipient
,updateBodyText
,uploadAttachment
,{{1如果草稿尚未准备好发送(即至少对收件人和主体有文本),我想在UI中禁用SEND按钮。我知道我不允许查询汇总,但它是唯一可以告诉我能够或无法发送电子邮件的人。
如果我要应用我对事件源和CQRS的了解,那么聚合将发出removeAttachment
事件,我的EmailIsReadyToBeSent
读取模型会选择并以某种方式更新UI但是,我必须在每个命令后检查并发送取消事件,即UserEmailDrafts
。
这感觉很复杂,你怎么看?
答案 0 :(得分:4)
除非收件人和正文接近应用逻辑,否则无法发送电子邮件这一事实,因为在一天结束时,更多的是在表单上填写字段而不是复杂域不变式。
我不会依赖于每次在屏幕上发生变化时查询读取模型的完整跨层往返,而是在UI中注入一些基本规则的知识,以便在收件人和按钮时立即重新启用按钮。身体是指定的。
当你看到客户端逻辑在表单上进行必要的字段验证时,实际上就像你并不感到震惊。由于逻辑简单而通用,因此它是一个完全有效且可接受的权衡。
请注意,这并不妨碍您在聚合中使用这些规则,拒绝任何不满足它们的命令。
答案 1 :(得分:1)
我将尝试使用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);
}
}
}
我们从回购中获取电子邮件,检查是否可以发送并发送。那么让我们看看聚合根(Email)如何使用不变量,这里是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 按钮时,或者当用户从一个输入切换时,它只从Aggregate Root调用方法canBeSent
(填写收件人)到另一个(填充身体)......这取决于你。