【问题标题】:DDD Entity validationDDD 实体验证
【发布时间】:2012-03-10 12:37:51
【问题描述】:

我有一个与实体验证相关的问题。例如,有一个User 可以注册到给定email 和password 的系统中。商业规则说:

  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。通过这种方式,规则在域中明确显示,使其更具表现力。

所以问题是:您如何看待这种方法?还有其他解决方案吗?

【问题讨论】:

    标签: validation domain-driven-design


    【解决方案1】:

    你可以实现一个域服务来封装它。通常在 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 方法设置为内部,这意味着创建用户的唯一方法是通过注册服务。

    【讨论】:

    • 如果两个同时的RequestUser-commands 调用这个UserRegistrationService 来注册一个具有相同电子邮件的用户,你最终可能会破坏你的一致性。聚合是执行一致性的可靠来源。当然这种同时发生的可能性不大,但如果发生了,你需要采取措施,否则你的系统就会被 bug 炸毁。
    【解决方案2】:

    在此示例中,您尝试执行三个验证:

    1. 电子邮件地址必须是有效格式;
    2. 电子邮件地址必须是唯一的(即,不存在拥有该电子邮件地址的现有用户);
    3. 密码必须符合一定的长度限制。

    上面的 1 和 3 是简单的验证,应该能够以声明方式对实体属性进行验证(例如,使用自定义属性和 .NET 中合适的验证库)。

    上面的 2 是一个棘手的问题,我认为这就是对 User 存储库的内在依赖。

    问题是:“防止创建与现有User 具有相同电子邮件地址的User 的责任在于User 实体吗?”。我相信这个问题的答案是“不”......“感觉”这个责任应该由更高级别的服务或实体承担,自然而然地了解整个用户集。

    所以,我的看法是:

    1. 将与用户一起进行的验证放在User 实体中(强凝聚力);
    2. 将唯一性约束放置在专门负责维护用户集的不变量的 DDD 服务中——它会通过包装唯一性检查和新 @987654326 的持久性来做到这一点@在事务中。

    【讨论】:

      【解决方案3】:

      您可以认为有两种验证:内部状态验证和上下文验证。您可以在该实体中执行内部验证,然后使用某些服务执行上下文验证。

      【讨论】:

      • 我喜欢这种方法!这样您就可以在域级别持有服务,对吗?
      【解决方案4】:

      马库斯,

      他的方法还不错,但我只是做得不同。

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

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

      我不清楚的另一件事是触发此验证的事件。是在实体用户被添加到存储库时,还是有实体的方法可以做到这一点?我将采用第二个选项调用方法 isValidAuthentication() 抛出异常。

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

      一个大大的拥抱和成功!

      【讨论】:

      • 感谢您的回答。关于异常抛出。我只是没有显示该代码。当然,一些 ValidationException 会被抛出给 User.Register() 的调用者。此方法将从作为应用程序服务的 SecurityService 调用。关于将验证移动到服务。这意味着如果在该服务之外调用 User.Register(),这可能会导致用户状态损坏,因为它无法控制其状态。
      • Markus,您如何看待用户身份验证服务?我这样做是为了降低夫妇。会“过度工程化”吗?
      • 对不起,我可能不太了解你。因此,您建议使用服务进行验证。在这种情况下,流程将如何?谁将调用 User.Register() 以及在哪里调用验证?
      • 我认为 Repository 应该注册 User 而不是他自己,而只是交易耦合,使 Repository 耦合到验证服务。我相信反过来会更好,创建一个负责服务验证的对象与用户实体的存储库合并。该对象可以是用户工厂或其他服务。
      • 这就是为什么我问这个策略是否会“过度设计”。有时我担心你想一次性应用设计模式。我已经阅读了很多关于 DDD 的内容,并且我觉得断言某些规则有点不安全。我曾与专家一起工作,我经常参考它,但只有实践才能使我达到完美。 ;)
      猜你喜欢
      • 2012-02-28
      • 1970-01-01
      • 1970-01-01
      • 2014-01-03
      • 2021-10-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多