在 DDD 中放置全局规则验证的位置

2022-08-31 13:31:46

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

所以,我们有实体用户

public class User : IAggregateRoot
{
   private string _name;

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

   // other data and behavior
}

和用户存储库

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

选项包括:

  1. 将存储库注入实体
  2. 将存储库注入工厂
  3. 在域服务上创建操作
  4. ???

每个选项更详细:

1 .将存储库注入实体

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

public User(IUserRepository repository)
{
    _repository = repository;
}

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

       _name = value; 
    }
}

更新:我们可以使用DI通过规范对象隐藏用户和IUser存储库之间的依赖关系。

2. 将存储库注入工厂

我可以把这个验证逻辑放在UserFactory中。但是,如果我们想更改现有用户的名称怎么办?

3. 在域服务上创建操作

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

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. ???

将实体的全局验证逻辑放在哪里?

谢谢!


答案 1

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

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

实体的主要关注点应该是跟踪业务状态 - 这已经足够了,他们不应该关心验证。

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
}

两种规格:

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);
    }
}

和验证器:

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));
    }

    // ...
}

为完整起见,这些接口:

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 中应用程序的规范模式感兴趣。


答案 2

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

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

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

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


推荐