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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/5818898/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-30 12:55:59  来源:igfitidea点击:

Where to put global rules validation in DDD

c#javadomain-driven-design

提问by Sergey Berezovskiy

I'm new to DDD, and I'm trying to apply it in real life. There is no questions about such validation logic, as null check, empty strings check, etc - that goes directly to entity constructor/property. But where to put validation of some global rules like 'Unique user name'?

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

So, we have entity User

所以,我们有实体用户

public class User : IAggregateRoot
{
   private string _name;

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

   // other data and behavior
}

And repository for users

和用户的存储库

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

Options are:

选项是:

  1. Inject repository to entity
  2. Inject repository to factory
  3. Create operation on domain service
  4. ???
  1. 将存储库注入实体
  2. 将存储库注入工厂
  3. 在域服务上创建操作
  4. ???

And each option more detailed:

每个选项更详细:

1 .Inject repository to entity

1 .将存储库注入实体

I can query repository in entities constructor/property. But I think that keeping reference to repository in entity is a bad smell.

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

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

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

       _name = value; 
    }
}

Update: We can use DI to hide dependency between User and IUserRepository via Specification object.

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

2. Inject repository to factory

2. 将仓库注入工厂

I can put this verification logic in UserFactory. But what if we want to change name of already existing user?

我可以把这个验证逻辑放在 UserFactory 中。但是如果我们想改变已经存在的用户的名字怎么办?

3. Create operation on domain service

3.创建域服务操作

I can create domain service for creating and editing users. But someone can directly edit name of user without calling that service...

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

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

4.???

Where do you put global validation logic for entities?

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

Thanks!

谢谢!

采纳答案by Marijn

Most of the times it is best to place these kind of rules in Specificationobjects. You can place these Specifications in your domain packages, so anybody using your domain package has access to them. Using a specification, you can bundle your business rules with your entities, without creating difficult-to-read entities with undesired dependencies on services and repositories. If needed, you can inject dependencies on services or repositories into a specification.

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

Depending on the context, you can build different validators using the specification objects.

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

Main concern of entities should be keeping track of business state - that's enough of a responsibility and they shouldn't be concerned with validation.

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

Example

例子

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

Two specifications:

两种规格:

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

And a validator:

还有一个验证器:

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

    // ...
}

For completeness, the interfaces:

为完整起见,接口:

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

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T subject);
}

Notes

笔记

I think Vijay Patel's earlier answer is in the right direction, but I feel it's a bit off. He suggests that the user entity depends on the specification, where I belief that this should be the other way around. This way, you can let the specification depend on services, repositories and context in general, without making your entity depend on them through a specification dependency.

我认为 Vijay Patel 之前的回答是正确的方向,但我觉得它有点偏离。他建议用户实体取决于规范,我认为这应该是相反的。通过这种方式,您可以让规范依赖于服务、存储库和上下文,而无需通过规范依赖使您的实体依赖它们。

References

参考

A related question with a good answer with example: Validation in a Domain Driven Design.

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

Eric Evans describes the use of the specification pattern for validation, selection and object construction in chapter 9, pp 145.

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

This article on the specification patternwith an application in .Net might be of interest to you.

文章的规格模式:在.NET应用程序可能是你的兴趣。

回答by George Polevoy

I would not recommend disallowing to change properties in entity, if it's a user input. For example, if validation did not pass, you can still use the instance to display it in user interface with validation results, allowing user to correct the error.

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

Jimmy Nilsson in his "Applying Domain-Driven Design and Patterns" recommends to validate for a particular operation, not just for persisting. While an entity could be successfully persisted, the real validation occurs when an entity is about to change it's state, for example 'Ordered' state changes to 'Purchased'.

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

While creating, the instance must be valid-for-saving, which involves checking for uniqueness. It's different from valid-for-ordering, where not only uniqueness must be checked, but also, for example, creditability of a client, and availability at the store.

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

So, validation logic should not be invoked on a property assignments, it should be invoked upon aggregate level operations, whether they are persistent or not.

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

回答by Niels van der Rest

Edit:Judging from the other answers, the correct name for such a 'domain service' is specification. I've updated my answer to reflect this, including a more detailed code sample.

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

I'd go with option 3; create a domain servicespecification which encapsulates the actual logic that performs the validation. For example, the specification initially calls a repository, but you could replace it with a web service call at a later stage. Having all that logic behind an abstract specification will keep the overall design more flexible.

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

To prevent someone from editing the name without validating it, make the specification a required aspect of editing the name. You can achieve this by changing the API of your entity to something like this:

为防止有人在未经验证的情况下编辑名称,请将规范作为编辑名称的必要方面。您可以通过将实体的 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.
    }
}

Your calling code would look something like this:

您的调用代码如下所示:

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

user.SetName("John", specification);

And of course, you can mock ISpecificationin your unit tests for easier testing.

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

回答by Vijay Patel

I would use a Specificationto encapsulate the rule. You can then call when the UserName property is updated (or from anywhere else that might need it):

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

It won't matter if another developer tries to modify User.Namedirectly, because the rule will always execute.

如果其他开发人员尝试User.Name直接修改并不重要,因为规则将始终执行。

Find out more here

在此处了解更多信息

回答by Kdeveloper

I'm not an expert on DDD but I have asked myself the same questions and this is what I came up with: Validation logic should normally go into the constructor/factory and setters. This way you guarantee that you always have valid domain objects. But if the validation involves database queries that impact your performance, an efficient implementation requires a different design.

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

(1) Injecting Entities:Injecting entities can be technical difficult and also makes managing application performance very hard due to the fragmentation of you database logic. Seemingly simple operations can now have an unexpectedly performance impact. It also makes it impossible to optimize your domain object for operations on groups of the same kind of entities, you no longer can write a single group query, and instead you always have individual queries for each entity.

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

(2) Injecting repository:You should not put any business logic in repositories. Keep repositories simple and focused. They should act as if they were collections and only contain logic for adding, removing and finding objects (some even spinoff the find methods to other objects).

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

(3) Domain serviceThis seems the most logical place to handle the validation that requires database querying. A good implementation would make the constructor/factory and setters involved package private, so that the entities can only be created / modified with the domain service.

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

回答by Roy Dictus

In my CQRS Framework, every Command Handler class also contains a ValidateCommand method, which then calls the appropriate business/validation logic in the Domain (mostly implemented as Entity methods or Entity static methods).

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

So the caller would do like so:

所以调用者会这样做:

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

Every specialized Command Handler contains the wrapper logic, for instance:

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

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

The ExecuteCommand method of the command handler will then call the ValidateCommand() again, so even if the client didn't bother, nothing will happen in the Domain that is not supposed to.

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

回答by Charles Lambert

Create a method, for example, called IsUserNameValid() and make that accessible from everywhere. I would put it in the user service myself. Doing this will not limit you when future changes arise. It keeps the validation code in one place (implementation), and other code that depends on it will not have to change if the validation changes You may find that you need to call this from multiple places later on, such as the ui for visual indication without having to resort to exception handling. The service layer for correct operations, and the repository (cache, db, etc.) layer to ensure that stored items are valid.

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

回答by SlavaGu

I like option 3. Simplest implementation could look so:

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

回答by epitka

Create domain service

Or I can create domain service for creating and editing users. But someone can directly edit name of user without calling that service...

创建域服务

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

If you properly designed your entities this should not be an issue.

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