在 DDD 中将全局规则验证放在哪里

Posted

技术标签:

【中文标题】在 DDD 中将全局规则验证放在哪里【英文标题】:Where to put global rules validation in DDD 【发布时间】:2011-08-14 16:53:08 【问题描述】:

我是 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 .将存储库注入实体

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

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

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

谢谢!

【问题讨论】:

【参考方案1】:

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

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

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

例子

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 中描述了使用规范模式进行验证、选择和对象构造。

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

【讨论】:

如果实体不依赖规范,你如何强制实体满足规范? @Marijn,感谢您的链接。但我在这里完全支持 Greg Young:“永远不要让你的实体进入无效状态”。我认为最好不要有任何无效的用户对象,然后忘记验证一个用户...... 我认为验证和防止您的对象进入无效状态是两件不同的事情。实体应该防止的无效状态,应该仅根据实体封装的信息来确定。当需要外部信息时(例如UserNameIsTaken),它不应该被实体封装。但这是我的观点……阅读对此的不同观点很有趣。 @Marijn 感谢您的回复!现在我再看一遍,我同意这种模式可以很好地用于复杂的业务规则实现。没有规范的 IValidator 的单个实现将提供对整个对象的简单属性验证。如果我要使用它,我可能会创建几个基类来支持简单验证器和基于规范的验证器的所有管道。所以,这比我想象的要灵活一些。嘿,+1 的答案。 :) 我喜欢这个规范模式,但我仍然有点不确定这个验证在模型的其余部分中的位置。在上面的示例中,哪个组件负责创建和使用 UserPersistenceValidator?应用服务?域服务?【参考方案2】:

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

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

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

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

【讨论】:

好点,但是 OP 应该把他的“全局验证”代码放在哪里? 我认为保存用户输入是视图模型的工作,而不是实体。然后聚合级操作将视图模型复制到实体。 @Marijn 我认为重点是没有“全局验证代码”。相反,验证体现在域模型的不同部分。例如。检查输入是否有效 -> 视图模型。检查 uniqe 实例 -> 模型/实体。恕我直言,验证问题可以像任何其他问题一样在整个域中切片 我明白了(并同意),但我发现它并没有真正回答这个问题。 @George Polevoy,您将如何编码您的验证逻辑以及将其放在哪里? @George Polevoy,我们绝对应该检查聚合级别的局部不变量。但是在哪里检查全局不变量呢? @Niels van der Rest,我认为视图模型不是业务规则验证的好地方【参考方案3】:

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

我会选择选项 3;创建一个 domain service 规范,该规范封装了执行验证的实际逻辑。例如,规范最初调用存储库,但您可以在稍后阶段将其替换为 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 以便于测试。

【讨论】:

那么,调用代码应该可以访问存储库吗?因此调用者应该是域或应用程序服务。那么,如果调用代码可以调用 userRepository.IsNameUnique("John"),那么向用户实体注入规范有什么好处? @lazyberezovsky:通过将规范作为SetName签名的一部分,调用代码可以立即看到名称必须遵循某个规范。如果依赖“外部”代码调用userRepository.IsNameUnique("John"),则无法确保名称实际上是唯一的,因为外部代码很容易省略对存储库的调用。 我真的很喜欢这种方法!它完美地融合了足够明确的特性,让消费代码在调用 User.SetName 时理解对 UniqueUserNameSpecification 的依赖,模拟规范的能力,以及足够“安全”,知道实体仍在负责用户名的验证,而无需依靠其他一些代码/层来调用它。例如。在 .Create() 方法调用期间依赖服务层调用规范。这样,您的实体将永远不会被允许进入无效状态,这是目标。 还有一个快速评论。当您去创建一个用户(通过构造函数)时,将无法传入 IUserRepository 以传递给 CreateUser 方法。在构造函数中将存储库传递给实体似乎是一件“坏事”,所以理论上,这个实体可以在无效状态下创建。我唯一能想到的就是允许使用空白名称属性创建 User 实体,并且在调用 User 构造函数后,使用 Make 的代码使用 SetName 方法为 User 分配名称。跨度> @indiecodemonkey 有几个differences between the Factory and Builder pattern,但是是的,这是一般的想法:)【参考方案4】:

我会使用 规范 来封装规则。然后,您可以在 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

【讨论】:

将验证放在 setter 中的缺点是它隐藏了对规范的依赖。更改属性也应该是轻量级的操作,如果规范调用数据库或 Web 服务则不是这种情况。 给猫剥皮的方法有很多种。重点是突出 规范 模式,并表明 DDD 不仅仅是 ServicesRepositories。我担心在 Services 或 Repositories 中转储太多方法最终会导致更弱的域模型 - 一个更难与领域专家沟通的模型(DDD 存在的原因)。 当然,much more to DDD 不仅仅是设计模式。但是暴露规范依赖对我来说似乎是一件好事,因为它清楚地表明名称不仅仅是任何字符串。您不妨走完整个九码并介绍UserName 课程。越强越好:) 上面代码的一个问题 - 在更改名称之前调用 IsSatisifiedBy(this)。所以,这将永远是真的。您可以先设置名称。但在这种情况下,您可以让 User 对象处于无效状态。 并发问题怎么办?实际上,每个(几乎)任何基于 HTTP 的客户端-服务器架构本质上都是并发的。规范可以通过两个同时运行的操作来实现,这“有点长”,在持久/保存之前,这些规范可能不再有效......那么,如何处理呢?【参考方案5】:

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

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

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

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

【讨论】:

(1) 我不认为性能取决于我们是从构造函数还是从调用者进行数据库调用——无论如何我们都需要进行调用。 (2) 我谈到了将存储库注入工厂。不可变对象看起来不错。但是如果需要在对象生命周期的后期检查全局规则,我们就不能使用工厂。 (3) 领域服务非常适合全局逻辑。但我认为域服务的目的是协调多个对象交互,所以也许它也是错误的地方? @lazyberezovsky:(1)我的偏好是保持构造函数的简单和无副作用,否则使用工厂。在处理 100 个实体时,并不总是需要同时进行 100 个单独的数据库调用,通常在处理大量实体时优化到更少的数据库往返。通过将所有逻辑放在实体中,您剥夺了自己处理不同于单个实体的大型实体集的可能性。 (2) 是的,但是你可以使用工厂,加上域服务。 @lazyberezovsky:(3) DDD 是关于领域的焦点,我们需要注意使其成为教条,因此我们应该始终牢记实现技术和(性能)要求的设计。顺便说一句,域服务似乎也适用于所有类型的模式,可用于实现协调实体操作的解决方案。【参考方案6】:

在我的 CQRS 框架中,每个 Command Handler 类还包含一个 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(),因此即使客户端没有打扰,域中也不会发生任何不应该发生的事情。

【讨论】:

【参考方案7】:

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

【讨论】:

那么,您建议使用选项(3),但使用单独的验证方法? 是的,只需从您的创建用户方法内部调用验证方法。如果返回 false,则抛出异常。【参考方案8】:

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

【讨论】:

【参考方案9】:

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

IsValid 方法:将实体转换为状态(可能无效)并要求它进行自我验证。

应用服务中的验证。

TryExecute 模式。

Execute / CanExecute 模式。

阅读更多here

【讨论】:

【参考方案10】:

创建域服务

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

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

【讨论】:

设计很简单 - 只是属性名称。 这就是我想说的,但也许不是……“很明显”:D -1 我认为这不是一个有用的答案。我不明白为什么它被赞成,

以上是关于在 DDD 中将全局规则验证放在哪里的主要内容,如果未能解决你的问题,请参考以下文章

自定义 Laravel 验证规则示例

验证码值放在session里安全吗

在 DDD 中将存储库实现保存在哪里?

在XML中将属性类型声明为实体时的验证错误

我可以在 vue.js 中将授权逻辑放在哪里?

干净的架构——在哪里放置输入验证逻辑? [关闭]