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

本文关键字:规则 验证 位置 全局 DDD | 更新日期: 2023-09-27 17:53:23

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

我们有实体User

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通过规范对象隐藏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。???

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

谢谢!

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

大多数情况下,最好将这类规则放在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之前的回答方向是正确的,但我觉得有点偏离。他建议用户实体取决于规范,而我认为应该相反。这样,你就可以让规范依赖于服务、存储库和上下文,而不必让你的实体通过规范依赖依赖于它们。

引用

一个相关的问题,有一个很好的例子:领域驱动设计中的验证。

Eric Evans在第9章第145页中描述了使用规范模式进行验证、选择和对象构造。

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

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

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

在创建时,实例必须是可保存的,这涉及到唯一性检查。它与valid-for-order不同,后者不仅必须检查惟一性,而且还必须检查客户端的可信性和商店的可用性。

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

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

我选择选项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,以便更容易地进行测试。

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

(1)注入实体:注入实体可能在技术上很困难,而且由于数据库逻辑的碎片化,还使管理应用程序性能变得非常困难。现在,看似简单的操作可能会对性能产生意想不到的影响。这也使得它不可能优化你的域对象上的操作相同类型的实体组,你不能再写一个单一的组查询,相反,你总是有单独的查询每个实体。

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

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

在这里了解更多

在我的CQRS框架中,每个命令处理程序类还包含一个ValidateCommand方法,然后调用域中适当的业务/验证逻辑(主要实现为实体方法或实体静态方法)。

所以调用者会这样做:

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(),因此即使客户端没有麻烦,也不会在域中发生不应该发生的事情。

总之你有4个选择:

  • IsValid方法:将一个实体转换到一个状态(可能无效),并要求它验证自己。

  • 应用程序服务中的验证。

  • TryExecute模式。

  • 执行/CanExecute模式。

阅读更多

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

我喜欢选项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");
    }
}

创建域服务

或者我可以为创建和编辑用户。但有人可以直接编辑用户名不调用该服务…

如果你正确地设计了你的实体,这应该不是一个问题。