【问题标题】:DDD - Life cycle of Value Objects: Validation & PersistenceDDD - 值对象的生命周期:验证和持久性
【发布时间】:2021-10-14 08:34:52
【问题描述】:

我了解什么是 VO(不可变、无身份……)。但我有几个问题来自与同事的讨论。

A) 验证 - 它应该有多精确?

我应该在 VO 中添加哪些类型的验证?只有basic 还是all? VO 的一个很好的例子是 Email 带有一些 regexp 验证。我已经看过很多次了。我曾开发过几个大/中型应用程序,而 regexp 还不够好,因为:

  • system-A:电子邮件的域名已验证,例如 test@gmali.com 在无效电子邮件中,因为域 gmali.com 不存在
  • system-B:我们有“临时电子邮件服务”的禁止域列表(服务),因为我们想避免“虚假帐户”

我无法想象将这种验证放入 VO,因为它需要网络通信,而且 VO 会很复杂(而且速度很慢)。

B) 验证:名称、标题、字符串...是 VO 的长度部分吗?

程序员可以使用旧的string 数据类型。我可以将 VO 成像为NotEmptyString,但是将值对象创建为:

  • FirstName(有长度限制的非空字符串)
  • Surname(有长度限制的非空字符串)
  • StreetName(有长度限制的非空字符串)

FirstName 和Surname 之间没有区别,因为在应用程序中我们无法确定是否有人在表格中交换了名字和姓氏。 Robert 可以是名字也可以是姓氏...

class Person 
{
  private string $firstName; // no VO for firstName
  // or 
  private FirstName $firstName;  // VO just for firstName & length validation
  // or
  private NotEmptyString $firstName; // VO - no max length validation
  // or
  private StringLength50 $firstName; // same as FirstName, different name for sharing 
}

哪种方法最好,为什么?

C) VO 副本:为实体参数提供“类型安全”?

这点和上一点类似。

这样创建类是否是一种好习惯:

class Surname extends Name
{

}

class FirstName extends Name
{

}

只是“化名”VO?

D) 持久性:读取存储的 VO

这一点与第一点密切相关:A) 验证 - 它应该有多精确?。我坚信存储在我的“存储引擎”(DB)中的内容是有效的——毫无疑问。当在“持久性步骤”期间验证所有内容时,我看不出有任何理由再次验证 VO。即使是复杂/写得不好的正则表达式也可能成为性能杀手——在用户电子邮件中列出 N 百个。 我迷路了……我应该只验证基本的东西并在持久化和读取期间使用相同的 VO,还是应该为这些情况使用 2 个单独的 VO?

E) 持久性/管理:系统中类似于“上帝”的东西。

根据我的经验:在实际系统中,具有更高权限的用户有时可以绕过验证规则,这又与 A) 点有关:

  • 您(作为系统的普通用户)可以在今天起 30 天内进行在线预订
  • 管理员用户可以在线预订任何日期

我应该只使用Date / FutureDate VO 还是什么?

F) 持久性:映射到数据库数据类型

将 VO 和 DB(存储引擎)数据类型紧密绑定是一种好习惯吗?

如果 FirstName 只能有 50 个字符,是否应该将其定义/映射到 VAR_CHAR(50)?

谢谢。

【问题讨论】:

  • 这真的应该分成几个不同的问题,而不是一个问题有六个部分。

标签: validation domain-driven-design value-objects


【解决方案1】:

A) 验证 - 它应该有多精确?

这不是关于精度,而是关于不变量和责任。值对象 (VO) 不可能对电子邮件地址是否存在具有权限。这是一个变化多端且无法由 VO 控制的事实。即使您有如下代码:

var emailAddress = EmailAddress.of('some@email.com', emailValidityChecker);

几分钟后地址可能不存在,用户可能永远丢失了他的帐户密码,等等。

那么EmailAddress 应该代表什么?它应确保地址的“格式”使其成为在您的域中可用且有用的地址。

例如,在一个负责发送纳税提醒的系统中,我有一个限制,即我必须使用 Exchange,并且它不能支持某些电子邮件格式,例如带有“本地部分中的前导、尾随或连续点”的地址(采纳了我的确切评论)。

尽管理论上这是一个技术问题,但这意味着我们的系统无法接收此类电子邮件地址,而且它们对我们完全没用,因此 ValidEmailAddress VO 不接受那些提前失败的电子邮件地址(否则会产生误报沿着链条)。

B) 验证:名称、标题、字符串...是 VO 的长度部分吗?

我愿意,尽管这样的长度有时会让人觉得有些武断或受基础设施驱动。但是,我认为可以肯定地说,500 个字符的名称肯定是错误的。此外,以合理的范围进行验证可以防止攻击(例如 1GB 名称???)。有些人可能会争辩说,这纯粹是一个基础设施问题,会将验证放在另一层,但我不同意,我认为这种区别是没有帮助的。

长度规则并不总是任意的,例如不能超过 280 个字符的 TweetMessage,这是一个域规则。

这是否意味着您必须为系统中的每个可能的字符串都有一个 VO?老实说,我反对害怕过度使用 VO 并倾向于对 VO 的痴迷而不是原始的痴迷,但几乎在每一种情况下,我都希望我只是花时间把那该死的绳子包起来。

要务实,但我认为使用不足比过度使用 VO 危害更大。

C) VO 副本:为实体参数提供“类型安全”?

我很可能不会仅仅为了在这里重用而扩展Name。很可能没有地方可以将Surename 与FirstName 互换,因此多态性也毫无用处。但是,显式类型可能有助于将“surename”与“first name”互换,反之亦然。

独立于显式类型是否有用,这里更有用的方法可能是将两者聚合到FullName VO 下,从而提高凝聚力。

请注意,过于严格的名称政策一直是许多国际系统的一大痛点...

D) 持久性:读取存储的 VO

持久化数据位于“安全”端,加载到内存时不应再次验证。在为我们的 VO 补水时,您应该能够绕过验证路径。

E) 持久性/管理:系统中的“上帝”之类的东西。

VO 非常适合强制执行它们的“不变量”。根据上下文的定义,不变量不会发生变化。当说“始终有效”的方法不起作用时,这实际上是许多人误解的东西。

也就是说,即使是系统管理员也很可能无法在过去进行新的预订,因此这可能是ReservationDate 的不变量。最终,您很可能会在它们所属的上下文中提取其他规则。

F) 持久性:映射到数据库数据类型

我认为在域中反映数据库限制比在数据库中反映域限制更重要。如果您的数据库只接受50 字符并且您超过了某些系统将崩溃并出现非常神秘的错误消息,甚至不会告诉您哪一列溢出。在域中进行验证将有助于更快地进行调试。但是,您不一定必须匹配数据库中的域规则。

【讨论】:

    【解决方案2】:

    DDD 与任何其他设计一样,是画线和制定抽象规则的问题。有些规则可能非常严格,而另一些规则可能在某种程度上流利。重要的是尽可能地保持一致性,而不是努力建立终极不可战胜的领域。

    验证 - 它应该有多精确?

    “重”验证不应在 VO 内部进行。一个VO不是很 它的本质不同于它所封装的原语,因此 验证应独立于外部因素。请记住 甚至像 byte 这样的原语也可以在内部进行验证:当字节变量的值大于 255 时会发生异常(有时甚至是编译错误)。

    另一方面,高级验证属于流程部分(用例/交互器/命令处理程序),因为它们涉及超出 VO 原语范围的操作,例如查询数据库或调用 API。例如,您可以从数据库中查询有效电子邮件提供商的列表,检查 VO 的提供商是否包含在列表中,如果不包含则抛出异常。这就是流程。

    当然,您可以决定在内存中创建一个电子邮件提供商的静态列表,在这种情况下,将其保存在 VO 中并根据该列表检查其原语是完全有效的。无需与外部世界交流,一切都是“本地”的。它可以扩展吗?可能不是。但它遵循 DDD 规则,即 VO 不应与外部资源“对话”。

    验证:名称、标题、字符串...是 VO 的长度部分吗?

    VO 与其他 DDD 概念非常相似,应该“大声说出”您的业务领域,这意味着它们应该表达业务语义。这就是为什么 FirstName、Surname 和 StreetName 是好名字,而 NotEmptyString 不太受欢迎,因为它传达的是技术细节而不是业务细节。

    例如,如果您的企业声明名称长度超过 50 个字符的客户与名称长度小于 50 个字符的客户的地址不同,那么您可能应该两个 VO,例如LongFirstName, ShortFirstName.

    确实,多个 VO 可能需要完全相同的验证,例如StreetName和CityName都必须以大写开头,长度不能超过100。这是否意味着我们必须努力避免以“可重用性”的名义重复?我会说不,特别是如果避免重复意味着拥有一个名为 CapitalHeadStringUpTo100Characters 的 VO。这样的名称不传达任何商业目的。此外,如果CityName 突然需要额外的验证,将CapitalHeadStringUpTo100Characters 拆分为两个 VO 可能需要做很多工作。

    VO 副本:为实体参数提供“类型安全”?

    继承是开发平台提供的一个工具,用起来还行,但只是到了乱七八糟或过于抽象的地步。请记住,VO 仅表示特定的领域方法原则。另一方面,当然可以在 DDD 应用程序中应用的多态 OOP 原则与抽象概念(即基类)紧密耦合,我想说它应该更适合实体模型部分。

    顺便说一句,您可以在网络上找到几个基本 VO 类的实现。

    持久性:读取存储的 VO

    如果在单个用例的不同点上一遍又一遍地进行相同的验证,那么系统设计的重要性就会降低。除非您有理由相信您的数据库可以被外部组件更改,否则无需重新验证即可从数据库重构实体就足够了。另请注意,典型实体可能会嵌入至少一个 VO,这与“持久性步骤”(构建实体时)和“读取步骤”(重构时)中使用的 VO 相同。

    持久性/管理:系统中的“上帝”之类的东西。

    多租户应用程序可以识别多种用户。软件不关心一个用户是否比另一个用户更强大,它只受制于规则。无论您选择将单个通用实体 User 与 VO FutureDate 设置为空,还是选择两个实体 User、Admin 与 VO FutureDate(非空)、FutureDate(可空)分别,我们在这里不太感兴趣。重要的是可以通过智能使用依赖注入来实现多租户:系统识别用户权限并推断要注入哪些工厂、服务或验证。

    持久性:映射到数据库数据类型

    这实际上取决于 DDD 领域的成熟度。应用程序总会有错误,如果您选择设计一个宽松的数据库,您应该对您的业务的错误容忍度有所了解。

    除此之外,请记住,无论您付出多少努力,您的数据库都可能永远无法反映完整的业务不变量集:将单个 VO 限制到一定长度很容易,但设置涉及多个 VO 的规则(即一个 VO 的有效性取决于另一个 VO)不太方便。

    【讨论】:

      猜你喜欢
      • 2021-09-21
      • 2011-05-18
      • 1970-01-01
      • 1970-01-01
      • 2013-02-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-28
      相关资源
      最近更新 更多