我最近开始玩DDD。今天我遇到了在我的应用程序中放置验证逻辑的问题。我不确定应该拿起哪一层。我在互联网上搜索,找不到解决我问题的统一解决方案。
让我们考虑以下示例。用户实体由ValueObjects表示,例如id(UUID),年龄和电子邮件地址。
final class User
{
/**
* @var \UserId
*/
private $userId;
/**
* @var \DateTimeImmutable
*/
private $dateOfBirth;
/**
* @var \EmailAddress
*/
private $emailAddress;
/**
* User constructor.
* @param UserId $userId
* @param DateTimeImmutable $dateOfBirth
* @param EmailAddress $emailAddress
*/
public function __construct(UserId $userId, DateTimeImmutable $dateOfBirth, EmailAddress $emailAddress)
{
$this->userId = $userId;
$this->dateOfBirth = $dateOfBirth;
$this->emailAddress = $emailAddress;
}
}
非业务逻辑相关验证由ValueObjects执行。这没关系。 我在进行业务逻辑规则验证时遇到了麻烦。
如果,假设我们需要让用户只有18岁以上才能拥有自己的电子邮件地址,该怎么办? 我们必须检查今天的年龄,如果不合适则抛出异常。
我应该把它放在哪里?
哪里放置负责检查存储库数据的验证工具?
喜欢电子邮件的唯一性。我读到了规范模式。如果我直接在Command Handler中使用它,可以吗?
最后,但并非最不重要。
如何将其与UI验证集成?
我上面描述的所有内容都是关于域级别的验证。但是让我们考虑从REST服务器处理程序执行命令。我的REST API客户端希望我返回有关输入数据错误时出错的完整信息。我想返回一个包含错误描述的字段列表。 我实际上可以在try块中包装所有命令准备并听取验证类型异常,但主要问题是它会在第一个异常之前向我提供有关单个错误的信息。 这是否意味着,我必须在控制器级复制我的验证逻辑(即使用zend-inputfilter - 我使用的是ZF2 / 3)?这听起来不存在......
提前谢谢。
答案 0 :(得分:4)
我会逐一回答你的问题,并在这里和那里给我两分钱以及我如何解决问题。
非业务逻辑相关验证由ValueObjects
执行
实际上,ValueObjects代表业务领域的概念,因此这些验证实际上也是业务逻辑验证。
实体 - 在构造函数中创建用户实体时检查它吗?
是的,在我看来,你应该尝试尽可能地在Aggregates中添加这种行为。如果将其放入命令或命令处理程序中,则会导致内聚性和业务逻辑泄漏到应用程序层中。我甚至会走得更远。问自己一个问题,即模型中是否存在未明确的隐藏概念。在您的情况下,AdultUser
和UnderagedUser
(他们可以都实现UserInterface
)实际具有不同的行为。在这些情况下,我总是努力明确地对此进行建模。
喜欢电子邮件的唯一性。我读到了规范模式。如果我直接在Command Handler中使用它,可以吗?
如果您希望能够将复杂查询与逻辑运算符(尤其是读取模型)结合使用,则规范模式很好。在你的情况下,我认为这是一个矫枉过正。将简单的containsUserForEmail($emailValueObject)
方法添加到UserRepositoryInterface
并从用例中调用此方法就可以了。
<?php
$userRepository
->containsUserForEmail($emailValueObject)
->hasOrThrow(new EmailIsAlreadyRegistered($emailValueObject));
如何将其与UI验证集成?
首先,应该对相关字段进行客户端验证。以正确的方式使用您的系统变得容易,并且难以以错误的方式使用它。
当然还需要服务器端验证。我们目前使用模式验证方法,其中我们有一个中央模式注册表,我们从中获取给定有效负载的模式,然后可以针对该JSON模式验证JSON有效负载。如果失败,我们返回一个序列化的ValidationErrors
对象。我们还通过Content-Type: application/json; profile=https://some.schema.url/v1/user#
标题告诉客户端如何构建有效的有效负载。
答案 1 :(得分:3)
只是为了扩展tPl0ch所说的内容,因为我觉得很有帮助......虽然我多年没有参加PHP堆栈,但无论如何这主要是理论上的讨论。
DDD实际应用中面临的一个较大问题是验证问题。传统的逻辑将要求验证必须存在于某个地方,它确实应该存在于任何地方。什么可能使人们惹恼了什么,当将它应用于DDD时,域名的质量永远不会在无效的状态下#34;。 CQRS已经解决了这个问题,并且您正在使用命令。
就个人而言,我这样做的方式是命令是改变状态的唯一方法。即使我需要为复杂的操作创建域服务,它也是完成工作的命令。传统的命令处理程序将针对聚合调度命令并将聚合放入过渡状态。所有这些都是相当标准的,但我还将转换验证的责任委托给命令本身,因为它们已经包含了业务逻辑。例如,如果我正在创建一个新帐户,并且我需要名字,姓氏和电子邮件地址,我应该在尝试通过以下方式尝试将其应用于聚合之前验证该命令是否存在于命令中。命令处理程序因此,我的每个命令处理程序不仅要知道命令,还要有命令验证程序。
此验证程序可确保命令的状态不会破坏域,从而允许我验证命令本身,并且在不产生与必须在基础结构或实现中的某处进行验证相关的额外成本的情况下。由于我必须改变状态的唯一方法仅仅是在命令中,我不会将任何逻辑直接放入域对象本身。这并不是说领域模型实际上是贫穷的,远非如此。假设如果您未在域对象本身中进行验证,则该域立即变得贫血。但是,聚合需要公开设置这些值的方法 - 通常通过方法 - 并且命令被转换为向该方法提供这些值。在您看到的半常见方法中,逻辑被放入属性设置器中,但由于您一次只设置一个属性,因此您可以更容易地将聚合保留为无效状态。如果您将该命令视为为了将该状态变为单个操作而进行验证,您会看到该命令是聚合的逻辑扩展(并且从代码组织的角度来看,如果不在下面,则非常接近,聚集体)。
由于此时我只处理命令验证,因此我通常也会进行持久性验证。基本上,就在聚合被持久化之前,聚合的整个状态将立即被验证。最终的目标是获得一个持久的命令,这意味着每个聚合将有一个持久性验证器,但是我有命令验证器。单个持久性验证器将提供绝对可靠的验证,即命令没有以违反总体域关注的方式改变聚合。它还会意识到单个聚合可以具有多个有效的过渡状态,这在命令中不易被捕获。通过多个状态,我的意思是聚合可能对于持久性有效,因为&#34;插入&#34;对于持久性,但可能不适用于&#34;更新&#34;操作。最简单的例子是我无法更新或删除尚未保留的聚合。
所有这些都可以在我自己的实现中浮出水面。 UI将数据传递给应用程序服务,应用程序服务将创建命令,并将调用&#34; Validate&#34;我的处理程序上的方法,它将返回命令中的任何验证失败而不执行它。如果存在验证错误,则应用程序服务可以向控制器屈服,返回它找到的任何验证错误,并允许它们浮出水面。此外,预先提交,数据可以发送,遵循相同的路径进行验证,并返回那些验证错误,而无需实际提交数据。这是两全其美的。如果用户提供无效输入,则可能经常发生命令违规。另一方面,持久性违规应该很少发生,如果有的话,在测试之外。这意味着命令正在以域不支持的方式改变状态。
最后,对命令进行后验证,应用程序服务可以执行它。我构建自己的基础结构的方式是命令处理程序知道命令是否在执行之前立即验证。如果不是,命令处理程序将执行由&#34;验证&#34;方法。然而,不同之处在于它将作为例外浮出水面。此时的目标是暂停执行,因为无效命令无法进入域。
虽然样本是Java(再次,不是我选择的平台),但我强烈推荐Vaughn Vernon&#34;实施域驱动设计&#34;。它确实吸引了埃文斯的许多概念。材料以及DDD范例的进步,例如CQRS + ES。至少对我来说,弗农的书中的材料,也是&#34; DDD系列的一部分&#34;书籍改变了我从根本上接近DDD的方式,就像蓝皮书向我介绍的那样。