구조체를 사용하여 내장 유형의 유효성 검사

일반적으로 도메인 개체에는 기본 제공 형식으로 표시 될 수 있지만 유효한 값은 해당 형식으로 표시 될 수있는 값의 하위 집합 인 속성이 있습니다.

이 경우 내장 유형을 사용하여 값을 저장할 수 있지만 입력 시점에서 항상 값의 유효성을 검사해야합니다. 그렇지 않으면 유효하지 않은 값으로 작업 할 수 있습니다.

이를 해결하는 한 가지 방법은 값을 기본 제공 유형의 struct단일 private readonly백업 필드가 있고 생성자가 제공된 값의 유효성을 검증 하는 사용자 정의로 저장하는 것 입니다. 그런 다음이 struct유형 을 사용하여 항상 검증 된 값만 사용할 수 있습니다.

또한 기본 제공 유형에서 캐스트 연산자를 제공하여 값이 기본 유형으로 원활하게 들어오고 나갈 수 있습니다.

도메인 개체의 이름을 나타내야하는 상황을 예로 들어 설명합니다. 유효한 값은 길이가 1 ~ 255 자 사이의 문자열입니다. 다음 구조체를 사용하여이를 나타낼 수 있습니다.

public struct ValidatedName : IEquatable<ValidatedName>
{
    private readonly string _value;

    private ValidatedName(string name)
    {
        _value = name;
    }

    public static bool IsValid(string name)
    {
        return !String.IsNullOrEmpty(name) && name.Length <= 255;
    }

    public bool Equals(ValidatedName other)
    {
        return _value == other._value;
    }

    public override bool Equals(object obj)
    {
        if (obj is ValidatedName)
        {
            return Equals((ValidatedName)obj);
        }
        return false;
    }

    public static implicit operator string(ValidatedName x)
    {
        return x.ToString();
    }

    public static explicit operator ValidatedName(string x)
    {
        if (IsValid(x))
        {
            return new ValidatedName(x);
        }
        throw new InvalidCastException();
    }

    public static bool operator ==(ValidatedName x, ValidatedName y)
    {
        return x.Equals(y);
    }

    public static bool operator !=(ValidatedName x, ValidatedName y)
    {
        return !x.Equals(y);
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        return _value;
    }
}

예 쇼는 투 string로 캐스팅 implicit이 아니라는 실패하지 않을 수 있지만 단으로 string같은 캐스팅 explicit이 같은 유효하지 않은 값을 던질 것이다, 그러나 물론이 두 수 중 수 implicit또는 explicit.

또한 from from cast를 통해서만이 구조체를 초기화 할 수 string있지만 IsValid static메소드를 사용하여 이러한 cast가 미리 실패하는지 테스트 할 수 있습니다 .

이것은 간단한 유형으로 표현 될 수있는 도메인 값의 유효성 검사를 시행하기에 좋은 패턴 인 것 같지만 자주 사용되거나 제안되는 것을 보지 못하고 그 이유에 관심이 있습니다.

제 질문은이 패턴을 사용할 때의 장단점과 그 이유는 무엇입니까?

이것이 나쁜 패턴이라고 생각한다면, 왜 그리고 당신이 느끼는 것이 최선의 대안인지 이해하고 싶습니다.

NB 나는 원래 Stack Overflow에 대해이 질문을 했지만 기본적으로 의견 기반 (철의 자체 주관적)으로 유지되었습니다. 더 많은 성공을 누릴 수 있기를 바랍니다.

위의 내용은 부분적으로 보류되기 전에받은 답변에 대한 응답으로 몇 가지 생각 아래에있는 원본입니다.

  • 대답에 의해 만들어진 주요 포인트 중 하나는 특히 많은 유형이 필요한 경우 위 패턴에 필요한 보일러 플레이트 코드의 양에 관한 것입니다. 그러나 패턴을 방어 할 때 템플릿을 사용하여 대부분 자동화 할 수 있으며 실제로 나에게 그렇게 나쁘지는 않지만 내 의견입니다.
  • 개념적 관점에서 C #과 같은 강력한 형식의 언어로 작업 할 때 강력한 형식의 원칙을 복합 값에만 적용하는 것이 아니라 복합 인스턴스에만 적용 할 수있는 값으로 확장하는 것이 아니라 내장 타입?


답변

이는 랩퍼 유형을 작성하는 것이 훨씬 쉬운 Standard ML / OCaml / F # / Haskell과 같은 ML 스타일 언어에서 일반적입니다. 두 가지 이점이 있습니다.

  • 이를 통해 코드 자체가 유효성 검사 자체를 처리하지 않고도 문자열이 유효성 검사를 받도록 강제 할 수 있습니다.
  • 한 곳에서 유효성 검사 코드를 지역화 할 수 있습니다. A는 경우 ValidatedName지금까지 잘못된 값을 포함, 당신은 오류에 알고 IsValid방법.

당신이 얻을 경우 IsValid방법 권리를, 당신은이 수신하는 기능을한다는 보장이 ValidatedName사실상이 검증 된 이름을 수신합니다.

문자열 조작을 수행해야하는 경우 문자열 (의 값 ValidatedName)을 가져오고 문자열 (새 값)을 리턴하고 함수 적용 결과의 유효성을 검증 하는 함수를 승인하는 공용 메소드를 추가 할 수 있습니다 . 따라서 기본 문자열 값을 가져와 다시 래핑하는 상용구가 제거됩니다.

래핑 값에 관련된 용도는 해당 값을 추적하는 것입니다. 예를 들어 C 기반 OS API는 때때로 자원 핸들을 정수로 제공합니다. 대신 OS API를 래핑하여 Handle구조 를 사용 하고 코드의 해당 부분에 대한 생성자에 대한 액세스 권한 만 제공 할 수 있습니다. Handles 를 생성하는 코드 가 올바른 경우 유효한 핸들 만 사용됩니다.


답변

이 패턴을 사용할 때의 장단점은 무엇이며 왜 그런가?

좋은 :

  • 자체 포함되어 있습니다. 유효성 검사 비트가 너무 많으면 덩굴손이 다른 위치에 도달합니다.
  • 자체 문서화에 도움이됩니다. 메서드 ValidatedString를 사용하면 호출의 의미에 대해 훨씬 명확 해집니다.
  • 공용 메소드간에 복제 할 필요없이 유효성을 한 지점으로 제한합니다.

나쁜 :

  • 캐스팅 마술은 숨겨져 있습니다. 관용적 인 C #이 아니므로 코드를 읽을 때 혼란을 일으킬 수 있습니다.
  • 던졌습니다. 유효성 검사에 맞지 않는 문자열을 갖는 것은 예외적 인 시나리오가 아닙니다. 이렇게 IsValid캐스트 전에하는 것은 조금 unweildy이다.
  • 왜 무언가가 잘못되었는지 말할 수 없습니다.
  • 기본값 ValidatedString은 유효하지 않습니다.

나는이 더 자주와 일의 종류 본 적이 UserAuthenticatedUser객체가 실제로 변경 일의 종류. C #에서는 적절하지 않은 것처럼 보이지만 훌륭한 접근 방법이 될 수 있습니다.


답변

당신의 길은 상당히 무겁고 집중적입니다. 일반적으로 다음과 같은 도메인 엔터티를 정의합니다.

public class Institution
{
    private Institution() { }

    public Institution(int organizationId, string name)
    {
        OrganizationId = organizationId;            
        Name = name;
        ReplicationKey = Guid.NewGuid();

        new InstitutionValidator().ValidateAndThrow(this);
    }

    public int Id { get; private set; }
    public string Name { get; private set; }        
    public virtual ICollection<Department> Departments { get; private set; }

    ... other properties    

    public Department AddDepartment(string name)
    {
        var department = new Department(Id, name);
        if (Departments == null) Departments = new List<Department>();
        Departments.Add(department);            
        return department;
    }

    ... other domain operations
}

엔터티의 생성자에서 FluentValidation.NET을 사용하여 유효성 검사가 트리거되어 잘못된 상태의 엔터티를 만들 수 없습니다. 속성은 모두 읽기 전용입니다. 생성자 또는 전용 도메인 작업을 통해서만 속성을 설정할 수 있습니다.

이 엔터티의 유효성 검사는 별도의 클래스입니다.

public class InstitutionValidator : AbstractValidator<Institution>
{
    public InstitutionValidator()
    {
        RuleFor(institution => institution.Name).NotNull().Length(1, 100).WithLocalizedName(() =>   Prim.Mgp.Infrastructure.Resources.GlobalResources.InstitutionName);       
        RuleFor(institution => institution.OrganizationId).GreaterThan(0);
        RuleFor(institution => institution.ReplicationKey).NotNull().NotEqual(Guid.Empty);
    }  
}

이 유효성 검사기는 쉽게 재사용 할 수 있으며 상용구 코드를 적게 작성합니다. 또 다른 장점은 읽을 수 있다는 것입니다.


답변

나는 가치 유형에 대한이 접근법을 좋아합니다. 개념은 훌륭하지만 구현에 대한 제안 / 불만이 있습니다.

캐스팅 :이 경우 캐스팅 사용이 마음에 들지 않습니다. 명시적인 from-string 캐스트는 문제가되지 않지만 (ValidatedName)nameValuenew 와는 큰 차이가 없습니다 ValidatedName(nameValue). 그래서 불필요한 것 같습니다. 암시 적 to-string 캐스트는 최악의 문제입니다. 실제 문자열 값을 얻는 것은 실수로 문자열에 할당 될 수 있고 컴파일러는 가능한 “정밀도 손실”에 대해 경고하지 않기 때문에보다 명시 적으로 생각해야합니다. 이러한 종류의 정밀 손실은 명백해야합니다.

ToString : ToString디버깅 목적으로 만 과부하를 사용 하는 것이 좋습니다. 그리고 나는 그것이 원가를 반환하는 것이 좋은 생각이라고 생각하지 않습니다. 이것은 암시적인 문자열 변환과 같은 문제입니다. 내부 가치를 얻는 것은 명백한 조작이어야합니다. 나는 당신이 구조를 외부 코드에 대한 일반적인 문자열처럼 행동하게하려고 노력하고 있다고 생각하지만 그렇게 생각하면 이런 종류의 유형을 구현함으로써 얻는 가치 중 일부를 잃어 가고 있다고 생각합니다.

같음GetHashCode : Structs는 기본적으로 구조적 같음을 사용합니다. 그래서 당신 EqualsGetHashCode이 기본 동작을 복제한다. 그것들을 제거 할 수 있으며 거의 ​​똑같을 것입니다.


답변