关于c#:在DDD中放置全局规则验证的位置

Where to put global rules validation in DDD

我是DDD的新手,我正试图在现实生活中应用它。没有关于这种验证逻辑的问题,如空检查,空字符串检查等 - 直接进入实体构造函数/属性。但是在哪里验证一些全局规则,如"唯一用户名"?

所以,我们有实体用户

1
2
3
4
5
6
7
8
9
10
11
12
public class User : IAggregateRoot
{
   private string _name;

   public string Name
   {
      get { return _name; }
      set { _name = value; }
   }

   // other data and behavior
}

和用户存储库

1
2
3
4
public interface IUserRepository : IRepository<User>
{
   User FindByName(string name);
}

选项包括:

  • 将存储库注入实体
  • 将存储库注入工厂
  • 在域服务上创建操作
  • ???
  • 每个选项更详细:

    1.将存储库注入实体

    我可以在实体构造函数/属性中查询存储库。但我认为在实体中保持对存储库的引用是一种难闻的气味。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public User(IUserRepository repository)
    {
        _repository = repository;
    }

    public string Name
    {
        get { return _name; }
        set
        {
           if (_repository.FindByName(value) != null)
              throw new UserAlreadyExistsException();

           _name = value;
        }
    }

    更新:我们可以使用DI通过Specification对象隐藏User和IUserRepository之间的依赖关系。

    2.将存储库注入工厂

    我可以将此验证逻辑放在UserFactory中。但是,如果我们想要更改现有用户的名称呢?

    3.在域服务上创建操作

    我可以创建用于创建和编辑用户的域服务。但有人可以直接编辑用户名而无需调用该服务...

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class AdministrationService
    {
        private IUserRepository _userRepository;

        public AdministrationService(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        public void RenameUser(string oldName, string newName)
        {
            if (_userRepository.FindByName(newName) != null)
                throw new UserAlreadyExistException();

            User user = _userRepository.FindByName(oldName);
            user.Name = newName;
            _userRepository.Save(user);
        }
    }

    4. ???

    您在哪里为实体设置全局验证逻辑?

    谢谢!


    大多数情况下,最好将这些规则放在Specification对象中。
    您可以将这些Specification放在域包中,这样任何使用域包的人都可以访问它们。使用规范,您可以将业务规则与实体捆绑在一起,而不会创建对服务和存储库具有不良依赖性的难以读取的实体。如果需要,您可以将服务或存储库的依赖项注入到规范中。

    根据上下文,您可以使用规范对象构建不同的验证器。

    实体的主要关注点应该是跟踪业务状态 - 这是一种责任,他们不应该关注验证。

    1
    2
    3
    4
    5
    public class User
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }

    两个规格:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class IdNotEmptySpecification : ISpecification<User>
    {
        public bool IsSatisfiedBy(User subject)
        {
            return !string.IsNullOrEmpty(subject.Id);
        }
    }


    public class NameNotTakenSpecification : ISpecification<User>
    {
        // omitted code to set service; better use DI
        private Service.IUserNameService UserNameService { get; set; }

        public bool IsSatisfiedBy(User subject)
        {
            return UserNameService.NameIsAvailable(subject.Name);
        }
    }

    还有一个验证器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class UserPersistenceValidator : IValidator<User>
    {
        private readonly IList<ISpecification<User>> Rules =
            new List<ISpecification<User>>
                {
                    new IdNotEmptySpecification(),
                    new NameNotEmptySpecification(),
                    new NameNotTakenSpecification()
                    // and more ... better use DI to fill this list
                };

        public bool IsValid(User entity)
        {
            return BrokenRules(entity).Count() > 0;
        }

        public IEnumerable<string> BrokenRules(User entity)
        {
            return Rules.Where(rule => !rule.IsSatisfiedBy(entity))
                        .Select(rule => GetMessageForBrokenRule(rule));
        }

        // ...
    }

    为了完整性,接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface IValidator< T >
    {
        bool IsValid(T entity);
        IEnumerable<string> BrokenRules(T entity);
    }

    public interface ISpecification< T >
    {
        bool IsSatisfiedBy(T subject);
    }

    笔记

    我认为Vijay Patel先前的答案是正确的方向,但我觉得它有点偏。他建议用户实体依赖于规范,我相信这应该是另一种方式。这样,您可以让规范依赖于服务,存储库和上下文,而不会让您的实体通过规范依赖性依赖它们。

    参考

    一个相关的问题,例如一个很好的答案:域驱动设计中的验证。

    Eric Evans在第9章第145页中描述了规范模式在验证,选择和对象构建中的使用。

    关于.Net中的应用程序的规范模式的这篇文章可能会引起您的兴趣。


    如果它是用户输入,我不建议不允许更改实体中的属性。
    例如,如果验证没有通过,您仍然可以使用实例在用户界面中显示验证结果,允许用户更正错误。

    Jimmy Nilsson在他的"应用领域驱动的设计和模式"中建议验证特定操作,而不仅仅是为了持久化。虽然可以成功保持实体,但实体验证会在实体即将更改其状态时发生,例如"已订购"状态更改为"已购买"。

    在创建时,实例必须是有效的保存,这涉及检查唯一性。它与有效订购不同,不仅要检查唯一性,还要检查客户的可信度和商店的可用性。

    因此,不应在属性赋值上调用验证逻辑,应在聚合级别操作时调用验证逻辑,无论它们是否持久。


    编辑:从其他答案判断,这种"域名服务"的正确名称是规范。我已经更新了我的答案以反映这一点,包括更详细的代码示例。

    我选择3;创建一个域服务规范,该规范封装了执行验证的实际逻辑。例如,规范最初调用存储库,但您可以在稍后阶段将其替换为Web服务调用。拥有抽象规范背后的所有逻辑将使整体设计更加灵活。

    为了防止某人编辑名称而不验证它,请使规范成为编辑名称的必要方面。您可以通过将实体的API更改为以下内容来实现此目的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    public class User
    {
        public string Name { get; private set; }

        public void SetName(string name, ISpecification<User, string> specification)
        {
            // Insert basic null validation here.

            if (!specification.IsSatisfiedBy(this, name))
            {
                // Throw some validation exception.
            }

            this.Name = name;
        }
    }

    public interface ISpecification<TType, TValue>
    {
        bool IsSatisfiedBy(TType obj, TValue value);
    }

    public class UniqueUserNameSpecification : ISpecification<User, string>
    {
        private IUserRepository repository;

        public UniqueUserNameSpecification(IUserRepository repository)
        {
            this.repository = repository;
        }

        public bool IsSatisfiedBy(User obj, string value)
        {
            if (value == obj.Name)
            {
                return true;
            }

            // Use this.repository for further validation of the name.
        }
    }

    你的调用代码看起来像这样:

    1
    2
    3
    4
    var userRepository = IoC.Resolve<IUserRepository>();
    var specification = new UniqueUserNameSpecification(userRepository);

    user.SetName("John", specification);

    当然,您可以在单元测试中模拟ISpecification以便于测试。


    我会使用规范来封装规则。然后,您可以在更新UserName属性时(或从可能需要它的任何其他位置)调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class UniqueUserNameSpecification : ISpecification
    {
      public bool IsSatisifiedBy(User user)
      {
         // Check if the username is unique here
      }
    }

    public class User
    {
       string _Name;
       UniqueUserNameSpecification _UniqueUserNameSpecification;  // You decide how this is injected

       public string Name
       {
          get { return _Name; }
          set
          {
            if (_UniqueUserNameSpecification.IsSatisifiedBy(this))
            {
               _Name = value;
            }
            else
            {
               // Execute your custom warning here
            }
          }
       }
    }

    如果另一个开发人员试图直接修改User.Name并不重要,因为规则将始终执行。

    在这里了解更多


    我不是DDD的专家,但我问自己同样的问题,这就是我提出的:
    验证逻辑通常应该进入构造函数/工厂和setter。这样,您可以保证始终拥有有效的域对象。但是,如果验证涉及影响性能的数据库查询,则有效的实现需要不同的设计。

    (1)注入实体:注入实体可能技术上很困难,并且由于数据库逻辑的碎片化,也很难管理应用程序性能。看似简单的操作现在可以产生意想不到的性能影响。它还使得无法优化域对象以便对同一类实体的组进行操作,您不再可以编写单个组查询,而是始终对每个实体都有单独的查询。

    (2)注入存储库:您不应该在存储库中放置任何业务逻辑。保持存储库简单且集中。它们应该像集合一样,只包含添加,删除和查找对象的逻辑(有些甚至将查找方法分离到其他对象)。

    (3)域服务这似乎是处理需要数据库查询的验证的最合理的地方。一个好的实现会使构造函数/工厂和setter涉及包私有,因此只能使用域服务创建/修改实体。


    在我的CQRS框架中,每个Command Handler类还包含一个ValidateCommand方法,然后该方法调用Domain中的相应业务/验证逻辑(主要实现为Entity方法或Entity静态方法)。

    所以调用者会这样做:

    1
    2
    3
    4
    5
    6
    if (cmdService.ValidateCommand(myCommand) == ValidationResult.OK)
    {
        // Now we can assume there will be no business reason to reject
        // the command
        cmdService.ExecuteCommand(myCommand); // Async
    }

    每个专用的Command Handler都包含包装器逻辑,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public ValidationResult ValidateCommand(MakeCustomerGold command)
    {
        var result = new ValidationResult();
        if (Customer.CanMakeGold(command.CustomerId))
        {
            //"OK" logic here
        } else {
            //"Not OK" logic here
        }
    }

    然后,命令处理程序的ExecuteCommand方法将再次调用ValidateCommand(),因此即使客户端没有打扰,在域中也不会发生任何事情。


    我喜欢选项3.最简单的实现看起来如此:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public interface IUser
    {
        string Name { get; }
        bool IsNew { get; }
    }

    public class User : IUser
    {
        public string Name { get; private set; }
        public bool IsNew { get; private set; }
    }

    public class UserService : IUserService
    {
        public void ValidateUser(IUser user)
        {
            var repository = RepositoryFactory.GetUserRepository(); // use IoC if needed

            if (user.IsNew && repository.UserExists(user.Name))
                throw new ValidationException("Username already exists");
        }
    }

    创建一个方法,例如,名为IsUserNameValid(),并使其可以从任何地方访问。我会把它放在用户服务中。这样做不会限制您在未来发生变化时的情况。它将验证代码保存在一个位置(实现),并且如果验证发生更改,则依赖于它的其他代码不必更改您可能会发现稍后需要从多个位置调用此代码,例如用于可视指示的ui无需诉诸异常处理。用于正确操作的服务层,以及用于确保存储的项有效的存储库(缓存,数据库等)层。


    Create domain service

    Or I can create domain service for
    creating and editing users. But
    someone can directly edit name of user
    without calling that service...

    如果您正确设计了实体,这应该不是问题。