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

时间:2011-04-28 12:52:47

标签: 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
}

用户存储库

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

选项包括:

  1. 将存储库注入实体
  2. 将存储库注入工厂
  3. 在域服务上创建操作
  4. ???
  5. 每个选项都更详细:

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

    4。 ???

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

    谢谢!

10 个答案:

答案 0 :(得分:56)

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

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

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

实施例

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

参考

一个相关的问题,答案很好,例如:Validation in a Domain Driven Design

Eric Evans描述了在chapter 9, pp 145中使用规范模式进行验证,选择和构造。

您可能会对.Net中的应用程序article on the specification pattern感兴趣。

答案 1 :(得分:11)

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

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

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

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

答案 2 :(得分: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.
    }
}

您的主叫代码如下所示:

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

user.SetName("John", specification);

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

答案 3 :(得分:3)

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

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,则无关紧要,因为该规则将始终执行。

Find out more here

答案 4 :(得分:2)

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

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

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

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

答案 5 :(得分:1)

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

所以调用者会这样做:

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

每个专用的命令处理程序都包含包装器逻辑,例如:

public ValidationResult ValidateCommand(MakeCustomerGold command)
{
    var result = new ValidationResult();
    if (Customer.CanMakeGold(command.CustomerId))
    {
        // "OK" logic here
    } else {
        // "Not OK" logic here
    }
}

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

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

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

答案 8 :(得分:0)

简而言之,您有 4 个选择:

  • IsValid 方法:将实体转换为状态(可能无效)并要求其验证自身。

  • 应用服务中的验证。

  • TryExecute 模式。

  • Execute / CanExecute 模式。

阅读更多here

答案 9 :(得分:-1)

  

创建域名服务

     

或者我可以为其创建域名服务   创建和编辑用户。但   有人可以直接编辑用户名   没有打电话给那个服务......

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