6

我最近开始玩 DDD。今天我在我的应用程序中放置验证逻辑时遇到了问题。我不确定我应该选择哪一层。我在互联网上搜索,找不到解决我问题的统一解决方案。

让我们考虑以下示例。用户实体由值对象表示,例如 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 岁以上的用户拥有自己的电子邮件地址怎么办?我们必须检查今天的年龄,如果不正常则抛出异常。

我应该把它放在哪里?

  • 实体 - 在构造函数中创建用户实体时检查它?
  • 命令 - 在执行插入/更新/任何命令时检查它?我在我的项目中使用战术家,所以它应该是一份工作
    • 命令
    • 命令处理程序

在哪里放置负责检查存储库数据的验证器?

就像电子邮件的唯一性。我阅读了规范模式。如果我直接在命令处理程序中使用它可以吗?

最后但并非最不重要。

如何将其与 UI 验证集成?

我上面描述的所有内容都是关于域级别的验证。但是让我们考虑从 REST 服务器处理程序执行命令。我的 REST API 客户端希望我在输入数据错误的情况下返回有关问题的完整信息。我想返回带有错误描述的字段列表。实际上,我可以将所有命令准备都包装在 try 块中并侦听 Validation 类型的异常,但主要问题是它会为我提供有关单个错误的信息,直到第一个异常。这是否意味着我必须在控制器级别复制我的验证逻辑(即使用 zend-inputfilter - 我正在使用 ZF2/3)?听起来很不协调...

先感谢您。

4

2 回答 2

6

我将尝试一一回答您的问题,并在这里和那里给我的两分钱以及将如何解决问题。

非业务逻辑相关的验证由 ValueObjects 执行

实际上 ValueObjects 代表来自您的业务领域的概念,因此这些验证实际上也是业务逻辑验证。

实体 - 在构造函数中创建用户实体时检查它?

是的,在我看来,您应该尝试在聚合中尽可能深入地添加这种行为。如果将其放入命令或命令处理程序中,则会失去凝聚力,并且业务逻辑会泄漏到应用程序层中。我什至会走得更远。问问自己这个问题,如果你的模型中有没有明确的隐藏概念。在您的情况下,实际具有不同行为的 anAdultUser和 an UnderagedUser(它们都可以实现 a )。在这些情况下,我总是努力明确地对此进行建模。UserInterface

就像电子邮件的唯一性。我阅读了规范模式。如果我直接在命令处理程序中使用它可以吗?

如果您希望能够将复杂的查询与逻辑运算符结合起来(尤其是对于读取模型),规范模式是很好的选择。在你的情况下,我认为这是一个矫枉过正。在用例中添加一个简单的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#标头告诉客户端它如何构建有效的有效载荷。

您可以在此处此处找到有关如何在 CQRS 架构之上构建 RESTful API 的一些不错的文章。

于 2016-08-04T15:58:31.807 回答
3

只是为了扩展 tPl0ch 所说的内容,因为我发现它很有帮助......虽然我已经很多年没有进入 PHP 堆栈,但无论如何,这主要是理论上的讨论。

DDD 在实际应用中面临的较大问题之一是验证问题。传统逻辑会规定验证必须存在于某个地方,它确实应该存在于任何地方。当将其应用于 DDD 时,可能最让人们绊倒的是域的质量永远不会“处于无效状态”。CQRS 已经在很大程度上解决了这个问题,而您正在使用命令。

就个人而言,我这样做的方式是命令是改变状态的唯一方法。即使我需要为复杂的操作创建域服务,也只有命令才能完成这项工作。传统的命令处理程序将针对聚合调度命令并将聚合置于过渡状态。所有这些都是相当标准的,但我还将验证转换的责任委托给命令本身,因为它们也已经包含业务逻辑。例如,如果我正在创建一个新帐户,并且我需要名字、姓氏和电子邮件地址,我应该验证它是否存在于命令中,然后再尝试通过命令处理程序。因此,我的每个命令处理程序不仅知道命令,

此验证器确保命令的状态不会危及域,这使我能够验证命令本身,并且在我不会因必须验证基础设施或实施中的某处而产生额外成本的情况下进行验证。由于我必须改变状态的唯一方法是完全在命令中,所以我不会将任何逻辑直接放入域对象本身中。这并不是说领域模型是贫乏的,实际上远非如此。有一个假设是,如果您没有在域对象本身中进行验证,那么域会立即变得贫乏。但是,聚合需要公开设置这些值的方法——通常是通过一种方法——并且命令被翻译以将这些值提供给该方法。您看到的半常见方法之一是将逻辑放入属性设置器中,但由于您一次只设置一个属性,您可以更轻松地使聚合处于无效状态。如果您将命令视为经过验证以将该状态作为单个操作进行变异,您会看到该命令是聚合的逻辑扩展(并且从代码组织的角度来看,它非常接近,如果不是在下面,总计的)。

由于此时我只处理命令验证,因此我通常也会进行持久性验证。本质上,就在聚合被持久化之前,聚合的整个状态将被立即验证。最终目标是让命令持久化,这意味着每个聚合将有一个持久性验证器,但命令验证器的数量与我拥有的命令一样多。该单个持久性验证器将提供可靠的验证,即该命令没有以违反总体域关注的方式改变聚合。它还将意识到单个聚合可以具有多个有效的过渡状态,这在命令中是不容易捕获的。通过多个状态,我的意思是聚合可能对持久性有效,作为持久性的“插入”,但可能对“更新”操作无效。最简单的例子是我无法更新或删除尚未持久化的聚合。

在我自己的实现中,所有这些都可以呈现在 UI 上。UI 会将数据交给应用程序服务,应用程序服务将创建命令,并在我的处理程序上调用“验证”方法,该方法将返回命令中的任何验证失败而不执行它。如果存在验证错误,应用程序服务可以让给控制器,返回它发现的任何验证错误,并允许它们浮出水面。此外,预提交,可以发送数据,遵循相同的验证路径,并返回那些验证错误,而无需实际提交数据。这是两全其美的。如果用户提供无效输入,则经常会发生命令违规。另一方面,持久性违规应该很少发生,如果有的话,在测试之外。

最后,命令验证后,应用程序服务可以执行它。我建立自己的基础设施的方式是命令处理程序知道命令是否在执行前立即得到验证。如果不是,则命令处理程序将执行“Validate”方法公开的相同验证。然而,不同之处在于它将作为一个例外出现。此时的目标是停止执行,因为无效命令无法进入域。

尽管这些示例是用 Java 编写的(同样,不是我选择的平台),但我强烈推荐 Vaughn Vernon 的“实现领域驱动设计”。它确实将 Evans 材料中的许多概念与 DDD 范式的进步(例如 CQRS+ES)结合在一起。至少对我而言,Vernon 书中的材料(也是“DDD 系列”书籍的一部分)从根本上改变了我接触 DDD 的方式,就像蓝皮书向我介绍的那样。

于 2016-08-05T21:24:29.937 回答