DDD实体验证

8
我有一个关于实体验证的问题。例如,有一个User可以通过提供emailpassword被注册到系统中。业务规则指出:
  1. email必须有效(必须符合电子邮件格式)并且唯一;
  2. password应该在6到20个字符之间。
我的初始想法是将验证放置在User.Register(email, password)中。这种方法的主要优点是User通过验证自身的正确性来控制如何注册。缺点是电子邮件唯一性验证需要调用UserRepository,所以User可能依赖于其Repository。为了解决这个问题,电子邮件和密码验证可能会被分解成一些类型的BusinessRule对象。因此,User.Register()方法中的验证可能是这样的:
var emailValidationErrors = _emailRule.Validate(email);
var passwordValidationErrors = _passwordRule.Validate(password);

在这种情况下,_emailRule_passwordRule可能作为构造函数参数传递:User(EmailRule emailRule, PasswordRule passwordRule)。

在这种情况下,User不直接耦合到UserRepository。以这种方式明确显示规则在域中,使其更具表现力。

那么问题是:您对这种方法有何看法?还有其他解决方案吗?

4个回答

4
您可以实现一个领域服务来封装这个过程。通常在DDD中,当业务逻辑超出一个聚合根的范围时,您会使用领域服务;在这种情况下,它是唯一性检查。所以,我会这样做:
public class UserRegistrationService : IUserRegistrationService
{
   private readonly IUserRespository _userRepository;

   public void Register(string email, string password)
   {
        if (!_userRepository.DoesEmailExist(email))
           throw new Exception("Email already registered");

        User user = User.Create(email, password);

        _userRepository.Save(user);
   }
}

此外,如果您担心User.Create在注册服务之外被调用,从而逃避唯一性检查,您可能会将User.Create方法设置为internal,这意味着创建用户的唯一方式是通过RegistrationService。

如果两个同时的“RequestUser”命令调用此“UserRegistrationService”以注册具有相同电子邮件的用户,则可能会破坏一致性。聚合是强制执行一致性的可靠来源。当然,这种同时性不太可能发生,但如果确实发生了,您需要采取措施,否则您的系统将被错误淹没。 - Luca Nate Mahler

4
在这个例子中,您正在尝试进行三个验证:
  1. 电子邮件地址必须是有效的格式;
  2. 电子邮件地址必须是唯一的(即,没有已存在的用户使用该电子邮件地址);
  3. 密码必须符合一定的长度限制。
其中1和3是简单的验证,应该可以通过实体属性声明性地完成(例如,在.NET中使用自定义属性和适当的验证库)。
而2则比较棘手,这正是我认为与 User 存储库内在依赖的地方。
问题是:“防止创建与现有用户具有相同电子邮件地址的 User 的责任归于 User 实体吗?”。我的答案是“不是”,因为它“感觉”像这个责任应该属于高级别服务或实体,对于整个用户集有完整的了解。
因此,我的看法是:
  1. 将那些与用户相关的验证放在 User 实体内(强内聚);
  2. 将唯一性约束放在 DDD 服务中,该服务专门负责维护 用户集 的不变量——它会通过包装唯一性检查和新 User 的持久化来执行此操作。

2

您可以将验证分为两种类型:内部状态验证和上下文验证。您可以从实体内部执行内部验证,然后使用某些服务执行上下文验证。


我喜欢这种方法!这样你是在领域层面上持有服务,对吧? - lcardosobr

1

马库斯,

他的方法并不差,但我会用不同的方式。

在我看来,您尊重了OCP,将验证规则放在实体之外,这是一个明智的决定。在类构造函数中使用这些验证规则,您是在暗示这些规则是不可变的,对吗?

我不会这样做,只需创建一个设置规则的方法,就像这个构造函数一样。对我来说,如果验证规则被违反了,不清楚会发生什么。我喜欢向用户界面抛出异常,以处理更广泛的警告。

另一件让我不清楚的事情是触发此验证的事件。它是当用户实体被添加到存储库中时触发,还是有一个实体方法会执行此操作?我会采取第二个选项,调用isValidAuthentication()方法并抛出异常。

关于实体对存储库的依赖,我敢说这是错误的。您甚至可以使实体依赖于存储库,因为存储库是对象的集合,这有什么问题吗?然而,在这一点上,似乎很清楚验证是一个服务。因此,如果我们将这些验证放在一个服务中,就可以消除这种耦合,并再次应用OCP。您同意吗?

祝您成功!


谢谢你的回答。关于异常抛出,我只是没有展示那段代码。当然,一些ValidationException会被抛给调用User.Register()的调用者。这个方法将从一个应用服务SecurityService中调用。关于将验证移动到服务中... 这意味着如果在该服务之外调用User.Register(),可能会导致用户状态损坏,因为它无法控制其状态。 - Markus
马库斯,你认为用户身份验证服务怎么样?我这样做是为了耦合降低。这会不会“过度工程化”? - lcardosobr
抱歉,我可能不太明白您的意思。那么您建议使用一个验证服务。在这种情况下,流程会是什么样子?谁会调用User.Register(),验证将在哪里被调用? - Markus
我认为仓库应该注册用户而不是自己,但只有交易耦合,使仓库与验证服务相耦合。我认为反过来会更好,创建一个负责服务验证的对象与用户实体的仓库合并。这个对象可以是用户工厂或其他服务。 - lcardosobr
这就是为什么我问这个策略是否会“过度设计”。有时候我担心你只想一次性应用所有的设计模式。我已经阅读了很多关于DDD的内容,但我对于断言某些规则感到有点不安。我曾经和一位专家合作过,并经常参考他的意见,但只有实践才能让我更加完美。;) - lcardosobr

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