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

Ser*_*kiy 62 c# java domain-driven-design

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

所以,我们有实体用户

public class User : IAggregateRoot
{
   private string _name;

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

   // other data and behavior
}
Run Code Online (Sandbox Code Playgroud)

和用户存储库

public interface IUserRepository : IRepository<User>
{
   User FindByName(string name);
}
Run Code Online (Sandbox Code Playgroud)

选项包括:

  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; 
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

4. ???

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

谢谢!

Mar*_*ijn 56

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

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

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

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

两个规格:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

还有一个验证器:

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

    // ...
}
Run Code Online (Sandbox Code Playgroud)

为了完整性,接口:

public interface IValidator<T>
{
    bool IsValid(T entity);
    IEnumerable<string> BrokenRules(T entity);
}

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T subject);
}
Run Code Online (Sandbox Code Playgroud)

笔记

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

参考

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

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

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

  • 我认为验证并防止您的对象进入无效状态是两个不同的事情.实体_should_阻止的无效状态应该仅从实体封装的信息中确定.当需要外部信息时(例如`UserNameIsTaken`),它不应该由实体封装.但是我的观点......阅读关于此的不同观点很有意思. (6认同)
  • 如果实体不依赖于规范,您如何*强制*实体满足规范? (2认同)
  • 我喜欢这个规范模式,但我仍然有点不确定这个验证适合模型的其余部分。在上面的示例中,哪个组件负责创建和使用 UserPersistenceValidator?应用服务?域服务? (2认同)

Geo*_*voy 11

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

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

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

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

  • 我认为持有用户输入而不是实体是视图模型的工作.然后,聚合级操作将视图模型复制到实体. (7认同)
  • @George Polevoy,我们肯定应该在总体水平上检查局部不变量.但是在哪里检查全局不变量?@Niels van der Rest,我不认为视图模型是业务规则验证的好地方 (3认同)
  • 好的,但OP应该把他的'全局验证'代码放在哪里? (2认同)
  • 我明白了(并同意),但我发现它并没有真正回答这个问题.@George Polevoy,你会如何_code_你的验证逻辑?你会把它放在哪里? (2认同)

Nie*_*est 7

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

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

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

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.
    }
}
Run Code Online (Sandbox Code Playgroud)

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

var userRepository = IoC.Resolve<IUserRepository>();
var specification = new UniqueUserNameSpecification(userRepository);

user.SetName("John", specification);
Run Code Online (Sandbox Code Playgroud)

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