领域驱动设计中如何在表单和值对象之间共享验证?

8

#1. 验证表单上的电子邮件地址

我有一个后端表单类,其中有一个 emailAddress 属性,具有验证逻辑,以便我可以向用户返回错误消息。我对所有表单输入进行验证,例如:

$form->fillWith($request->input());

if($form->validate()){
    $form->dispatch($command); // if synchronous, form takes command's messageBag
}

return response($form->getMessageBag()->toJson());

#2. 在Command Handler中验证EmailAddress数值对象

我有一个命令处理程序,将获取原始字符串电子邮件并创建一个值对象。如果电子邮件无效,则该值对象在创建时会引发异常:

public function handle($command){

   try {
      $emailAddress = new ValueObjects\EmailAddress($command->emailAddress);

      // create more value objects...

      // do something else with the domain...

   } catch (DomainException $e) {
        $this->messageBag->add("errors", $e->getMessage());
   } catch (\Exception $e) {
        $this->messageBag->add("errors", "unexpected error");
   }

   return $this->messageBag;
}

在#1中,我希望在分派命令之前尽早捕获验证。但是在#2中,当我构建VO时,验证逻辑会重复。

我的问题:

  • 如果我需要更改电子邮件地址的验证要求,则必须更新两个位置。
  • 如果我在表单上使用VO,则在传递给命令时必须再次解构它们。此外,如果我的表单位于不同的Bounded Context中,则VO将泄漏来自其他Bounded Context的域(也许这是必要的?)。

因此,我的问题是,我应该创建一些验证器对象,使我的表单验证和VO可以共享/利用吗?或者如何捕获表单和值对象之间重复的验证问题?


1
这个答案可能会有所帮助...请阅读"How to report back..."部分。 - plalx
@plalx 好的。听起来我只需要使用一些表单验证器对象来处理UI中不同的入口点。然后创建我的值对象时不包含这些验证器,就不用担心重复逻辑了。如果电子邮件要求发生变化,那么我需要更新两个地方而不是一个,但正如你所说,我是一个全栈工程师。 - prograhammer
如果你想把这个转化成一个答案,那么我可以接受它。你帮助我获得了所需的洞察力,让我能够采取舒适的方法。谢谢! - prograhammer
1
好的,我现在没有时间,但很高兴能帮忙!顺便说一下,如果验证逻辑非常复杂以至于您真的不想有任何重复,那么您可以公开一个验证器 Web 服务,供 UI 调用。例如,如果您有密码复杂性策略,但是根据帐户类型有各种策略,那么复制所有这些策略就不太实际了。 - plalx
1个回答

6
将验证逻辑封装到可重用的类中。 这些类通常称为规范验证器规则,并且是域的一部分
有多种方法可以实现这一点,以下是我使用的方法:
  1. 定义一个接口Specification,提供一个bool IsSatisifed()方法。
  2. 为特定的值对象(例如EmailWellformedSpec)实现此接口。
  3. 通过使用规范作为前提条件在域内执行业务规则(即违反规则始终是编程错误)。
  4. 在服务层中使用规范进行输入验证(即违反规则是用户错误)。
如果您想将多个规范组合成一个更大的规范,则规范模式是一个不错的方法。请注意,如果使用该模式,您需要通过构造函数传递数据,但这不是问题,因为规范类通常很简单。

我有点迷失了@theDmi。你是说我可以提取重复的验证部分,这些部分在我的后端表单类和值对象中,并将它们放入单个规范中?该规范可能直接用于表单,也可能用于值对象内部?因此,我可以对EmailWellFormedSpec实现进行任何更改,而无需更改其接口,这样我的表单和VO都可以保持DRY(即使验证仍将运行两次,但这不是SOLID问题,只是微不足道的性能下降)。 - prograhammer
@prograhammer 是的,你说对了。我唯一不会做的事情就是在值对象内部使用规范,因为这样会使它们难以使用(你需要在构造函数中设置规范)。我会在值对象使用的领域中使用规范,例如实体的方法中。 - theDmi
哦,你不喜欢在VO的构造函数中放置规范吗?看起来规范是EmailAddress VO所包含的不变量的一部分,对吧?此外,我考虑让实体方法要求完全形成的VO(而不是a.primitive字符串或EmailWellFormedSpec等)。否则,我的实体方法将有一个更大的参数集。例如,考虑地址。那是5个字段。但也许我明白你的建议了,即VO可能应该隐藏在聚合根后面? - prograhammer
2
我的建议是不要将原始类型传递给实体方法。应该传递值对象,并且将传递的VO有效性验证与规范相匹配作为方法的前提条件 - theDmi
抱歉,我现在用手机打字,手指受限!明天我会再发一条评论来进行最后的澄清,然后我会给你点赞。感谢你在这里的回复!在我回去之前,我需要再次审查规范模式。 - prograhammer
2
@theDmi,我不太明白。您建议将VO与其要满足的规范一起传递,并在实体方法中进行检查?如果我有多个操作此VO的方法,您会在每个方法中执行相同的检查吗?如果它只是一个包装值而没有任何验证,那么这个VO与原始类型有什么区别?您能否分享一些上述内容的代码片段? - Kamil Latosinski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接