领域驱动设计中的验证

Ank*_*jay 9 c# design-patterns domain-driven-design

我们的团队正在按照领域驱动设计 (DDD) 启动一个新项目。在高层次上,我们在域的顶部有一个 API,它使客户端能够在域上执行操作。我不太清楚的问题之一是我们在哪里对 DDD 中的某个属性/属性执行验证。

考虑这个例子。让我们说,我的 API 公开了以下数据合同/ DTO:

 public class Person
 {
    public string Email { get; set;}
    public string Name { get; set; }
 }
Run Code Online (Sandbox Code Playgroud)

现在,假设我们有业务验证,可以防止用户输入无效的电子邮件地址并限制用户的姓名超过 50 个字符。

为此,我可以看到以下三种方法:

方法 1 中,我们仅在 API 上进行数据验证(通过数据注释或 Fluent 验证)。我不会在我的域中重复验证。从理论上讲,这可能意味着我的域可能处于无效状态。但是,由于入口点 (API) 正在验证,因此在实际场景中是不可能的。

在此处输入图片说明

方法 2 中,我们在 API 和我的域中进行数据验证。这种方法帮助我们完全消除了我的域和 API 之间的耦合。API 可以独立向客户端返回 Bad 请求。并且由于域再次执行验证,域不可能进入无效状态。然而,在这种方法中,我们违反了 DRY 原则。

在此处输入图片说明

方法 3 中,我们只在 Domain 进行验证,而不在 API 级别对 DTO 进行验证。使用这种方法,虽然我们不重复验证,但当 API 调用试图将域置于无效状态时,域不会抛出异常。相反,我们需要将该异常包装在某个Result对象中。这将有助于 API 向客户端发送适当的响应(例如,错误请求而不是内部服务器错误)。我不喜欢这种方法的一点是我更愿意抛出一个硬异常而不是放置一个包装器。 在此处输入图片说明

  • 哪种方法最有意义,为什么?

  • 业务验证和业务规则之间的界限在哪里?(假设业务规则存在于域中)。

  • 有什么明显的东西我在这里遗漏了吗?

注意:这个问题可能看起来类似于 域驱动设计中的验证, 并且应该将输入验证放在域驱动设计中的什么位置?但它并没有真正回答细节。

gui*_*e31 6

从理论上讲,这可能意味着我的域名可能会处于无效状态。

我认为名称长度超过 50 个字符并不意味着域状态无效...该域仍然可以完美运行。

您必须区分输入验证(数据是否适合技术槽?)和领域不变量。有些事情可以在 API 级别验证,并且与域无关,而其他事情则必须通过加载域数据进行检查,因此外层无法轻松访问。

它们实际上是两套(或多套)不同的规则,比您想象的更加相互独立。

TL;DR - 没有硬性且快速的答案。尝试比“业务验证”更深入地描述事物,并根据规则类型明智地选择方法 1、2 或 3 。


Ank*_*jay 6

我已经阅读了这里的专家提供的答案,经过团队内部的深思熟虑,我们决定在考虑以下关键原则的情况下实施验证。

  • 我们将 API 验证和域验证/业务规则视为单独的关注点。我们以与没有 DDD 的情况相同的方式进行 API 验证。这是因为,API 只是与我们的域对话的接口。明天,我们可以有一个消息总线介于两者之间,但我们的域逻辑不会改变。API 验证包括但不限于字段长度检查、正则表达式检查等。

  • 域验证仅限于业务规则。诸如长度检查、正则表达式等对业务没有任何意义的字段级验证不受业务规则的约束。如果存在违反业务规则的行为,我们会简单地抛出一个DomainException而不关心消费者将如何处理它。在 API 响应的情况下,简单的意思是将状态代码500返回给客户端。

  • 因为,我们现在将 API 验证和业务规则视为一个单独的问题,这确实意味着有可能重复某些验证规则。但我们对此表示同意,因为它使事情变得更简单和容易推理。由于它们是松散耦合的,我们可以轻松地独立更改 API 验证或业务规则,而不会影响另一个。

  • 很少有边界案例email可以ValueObject在我们的域中争论。但我们会逐案处理。例如,我们目前在我们的域中没有看到任何值具有单独的ValueObjectforemail字段。因此,它仅在 API 级别进行验证(正则表达式检查)。如果将来如果企业提出其他规则,例如限制到特定域,那么我们可能会重新考虑我们的决定。

牢记上述原则帮助我们保持简单和易于推理。


Voi*_*son 5

现在,假设我们有业务规则,可以防止用户输入无效的电子邮件地址并限制用户的姓名超过 50 个字符。

本例中需要注意的重要事项

  • 您无权访问此数据。用户名和电子邮件地址均在其他地方分配和管理
  • 就领域模型而言,这些数据是不透明的;您可能永远不会操纵它,或根据内容改变任何计算。就您的业务规则而言,它们只是您正在复制的哈希值,以便您可以将它们传递给其他东西(在信封上打印姓名,或发送电子邮件)。

从语义上讲,这两个值基本上都是“标识符”的风格。

既然如此,领域模型根本不关心验证,除了内存不足之类的问题。如果您有固定长度的列或类似的内容,您的数据模型可能会关心。

因此,这很可能是您在消息边界上关心的地方之一,但不是在域本身内。

但它并不能很好地解决验证可能存在于何处的一般问题。

将此情况与存款金额之类的东西进行对比 - 它是一个数字,您可以合理地期望将其与其他数字相加/减去,将其与其他数字进行比较,等等。在那里,您可能会看到类似的内容Integer.MAX,并合理地得出结论,攻击/数据输入错误比真正的用例更有可能发生,因此您将完全消除该选项。

消息边界的验证主要由以下问题驱动:您可以信任源吗?如果有任何疑问,那就没有疑问了。(Deogun 和 Johnson 的《域驱动安全》是一个很好的起点)。

很大程度上,消息边界的验证归结为确定您收到的字节序列实际上符合消息模式;这当然可以包括对允许值范围的限制。(示例:HTTP 响应包含状态代码,但您无需假装状态代码 777 的响应旨在改善您的下午生活)。

因此,声明消息中的名称字段不超过 50 个字符,并且消息中的电子邮件地址字段符合RFC 5322 中 addr_spec的定义是完全合理的。

然后在边界处,确保获得的字节实际上满足消息约束,如果满足则将其传递很长。

但在领域模型内呢?如果您不需要对数据做出假设,那就完成吧。“应用程序说这些是字节?对我来说足够好了!”

从技术角度来说,关键测试是领域模型是否有任何需要满足的先决条件,以确保其结果的正确性。 如果我们有先决条件,那么验证可以作为检测违规行为的受控方式。

但是,通过不需要的前置条件检查来注入域模型并没有太多的增值功能。

(再次,与域模型形成鲜明对比的amount是,领域模型非常有兴趣在开始不加区别地转移资金之前检测违规行为)。

  • 您好@voiceofunreason,感谢您的详细解释。我从这里得到的最重要的关键知识之一是“前提条件”。 (2认同)