Program Tip

전역 규칙 유효성 검사를 DDD에 넣을 위치

programtip 2020. 11. 28. 10:16
반응형

전역 규칙 유효성 검사를 DDD에 넣을 위치


나는 DDD를 처음 접했고 그것을 실제 생활에 적용하려고 노력하고 있습니다. 엔티티 생성자 / 프로퍼티로 직접 이동하는 null 검사, 빈 문자열 검사 등과 같은 유효성 검사 논리에 대한 질문은 없습니다. 그러나 '고유 사용자 이름'과 같은 일부 전역 규칙의 유효성 검사를 어디에 두어야할까요?

따라서 엔티티 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를 사용하여 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. ???

엔터티에 대한 전역 유효성 검사 논리를 어디에 배치합니까?

감사!


대부분의 경우 이러한 종류의 규칙을 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은 "도메인 기반 설계 및 패턴 적용"에서 지속성뿐만 아니라 특정 작업에 대한 유효성을 검사 할 것을 권장합니다. 엔티티가 성공적으로 지속될 수 있지만 실제 유효성 검사는 엔티티가 상태를 변경하려고 할 때 발생합니다. 예를 들어 '주문 됨'상태가 '구매 됨'으로 변경됩니다.

생성하는 동안 인스턴스는 저장을 위해 유효해야하며 고유성을 확인해야합니다. 고유성뿐만 아니라 고객의 신용도, 매장에서의 가용성 등도 확인해야하는 주문 유효와 다릅니다.

따라서 유효성 검사 논리는 속성 할당에서 호출되어서는 안되며, 지속 여부에 관계없이 집계 수준 작업에서 호출되어야합니다.


편집 : 다른 답변에서 볼 때 그러한 '도메인 서비스'의 올바른 이름은 사양 입니다. 더 자세한 코드 샘플을 포함하여이를 반영하기 위해 내 답변을 업데이트했습니다.

옵션 3을 선택하겠습니다. 유효성 검사를 수행하는 실제 논리를 캡슐화 하는 도메인 서비스 사양을 만듭니다 . 예를 들어 사양은 처음에 저장소를 호출하지만 나중에 웹 서비스 호출로 바꿀 수 있습니다. 추상 사양 뒤에 모든 논리가 있으면 전체 디자인을보다 유연하게 유지할 수 있습니다.

다른 사람이 이름을 검증하지 않고 편집하지 못하도록하려면 사양을 이름 편집의 필수 요소로 만드십시오. 엔티티의 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 ISpecification in your unit tests for easier testing.


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

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.Name directly, because the rule will always execute.

Find out more here


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.

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

(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).

(3) Domain service This 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.


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

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.


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.


I like option 3. Simplest implementation could look so:

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

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.

참고URL : https://stackoverflow.com/questions/5818898/where-to-put-global-rules-validation-in-ddd

반응형