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


9

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

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

이를 해결하는 한 가지 방법은 값을 기본 제공 유형의 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 #과 같은 강력한 형식의 언어로 작업 할 때 강력한 형식의 원칙을 복합 값에만 적용하는 것이 아니라 복합 인스턴스에만 적용 할 수있는 값으로 확장하는 것이 아니라 내장 타입?

bool (T) lambda
ratchet freak

답변:


4

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

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

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

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

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


1

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

좋은 :

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

나쁜 :

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

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


1
고맙게도 네 번째 "con"은 아직 가장 강력한 논거라고 생각합니다. 기본 또는 유형의 배열을 사용하면 유효하지 않은 값을 줄 수 있습니다 (0 / null 문자열이 유효한 값인지 여부에 따라 다름). 이것들은 잘못된 값으로 끝나는 유일한 두 가지 방법입니다. 그러나이 패턴을 사용하지 않으면이 두 가지로 인해 여전히 유효하지 않은 값을 얻을 수 있지만 적어도 그 값을 확인해야한다고 생각합니다. 따라서 이것은 기본 유형의 기본값이 유형에 유효하지 않은 접근법을 잠재적으로 무효화 할 수 있습니다.
gmoody1979

모든 단점은 개념의 문제가 아닌 구현 문제입니다. 또한 나는 "예외는 예외적이어야한다"라는 애매 모호한 개념을 발견했다. 가장 실용적인 방법은 예외 기반 및 비 예외 기반 방법을 모두 제공하고 호출자가 선택할 수 있도록하는 것입니다.
Doval

@Doval 다른 의견에서 언급 한 것을 제외하고는 동의합니다. 패턴의 요점은 ValidatedName이있는 경우 반드시 유효해야한다는 것을 확실히하는 것입니다. 기본 유형의 기본값이 도메인 유형의 유효한 값이 아닌 경우에도 분류됩니다. 이것은 물론 도메인에 따라 다르지만 숫자 유형보다 문자열 기반 유형의 경우가 많습니다 (생각했을 것입니다). 패턴은 기본 유형의 기본값이 도메인 유형의 기본값으로도 적합한 경우에 가장 잘 작동합니다.
gmoody1979

@Doval-동의합니다. 개념 자체는 훌륭하지만 구체화 유형을 지원하지 않는 언어로 구체화하려고 시도하고 있습니다. 항상 구현 문제가 있습니다.
Telastyn

말했듯이, "outbound"캐스트와 구조체의 메소드 내에서 필요한 다른 곳에서 기본값을 확인하고 초기화되지 않은 경우 던질 수는 있지만 어지럽기 시작한다고 가정합니다.
gmoody1979

0

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

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

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


downvoter가 내 대답이 downvoted 된 이유를 설명하려고주의를 기울입니까?
L-Four

질문은 값 유형을 제한하는 구조체에 관한 것이며 WHY를 설명하지 않고 클래스로 전환했습니다. (
교섭자가

나는 이것이 더 나은 대안을 찾는 이유를 설명했으며 이것이 그의 질문 중 하나였습니다. 답장을 보내 주셔서 감사합니다.
L-Four

0

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

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

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

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


캐스팅 : 의미 적으로 이것은 새로운 ValidatedName을 생성하는 대신 문자열을 ValidatedName으로 변환하는 것처럼 느껴집니다. 기존 문자열을 ValidatedName으로 식별하고 있습니다. 따라서 나에게 캐스트는 의미 적으로 더 정확 해 보입니다. 타이핑에는 약간의 차이가 있습니다 (키보드 종류의 손가락). to-string 캐스트에 동의하지 않습니다. ValidatedName은 문자열의 하위 집합이므로 정밀도를 잃을 수는 없습니다.
gmoody1979

ToString : 동의하지 않습니다. 나에게 ToString은 요구 사항에 적합하다고 가정 할 때 디버깅 시나리오 외부에서 사용할 수있는 완벽한 방법입니다. 또한 유형이 다른 유형의 하위 집합 인 상황에서는 가능한 한 하위 집합에서 수퍼 세트로 기능을 쉽게 변환 할 수 있으므로 사용자가 원하는 경우 거의 다음과 같이 처리 할 수 ​​있습니다. 수퍼 셋 타입, 즉 문자열 ...
gmoody1979

같음 및 GetHashCode : 예 구조체는 구조적 같음을 사용하지만이 경우 문자열 값이 아닌 문자열 참조를 비교합니다. 따라서 Equals를 재정의해야합니다. 기본 유형이 값 유형 인 경우에는 이것이 필요하지 않을 것에 동의합니다. 값 유형에 대한 기본 GetHashCode 구현에 대한 나의 이해 (아주 제한적 임)는 동일한 값을 제공하지만 성능이 향상됩니다. 나는 그것이 사실인지 실제로 테스트해야하지만 질문의 요점에 약간의 부수적 인 문제입니다. 그나저나 답해 주셔서 감사합니다 :-).
gmoody1979

@ gmoody1979 구조는 기본적으로 모든 필드에서 같음을 사용하여 비교됩니다. 문자열에 문제가 없어야합니다. GetHashCode와 동일합니다. 구조는 문자열의 하위 집합입니다. 유형을 안전망으로 생각하고 싶습니다. ValidatedName으로 작업하고 싶지 않고 실수로 문자열을 사용하려고합니다. 컴파일러가 확인되지 않은 데이터로 작업하고 싶다고 명시 적으로 지정하면 선호합니다.
Euphoric

죄송합니다. Equals에 대한 좋은 지적입니다. 기본 동작이 비교를 수행하기 위해 리플렉션을 사용해야하는 경우 재정의가 더 잘 수행되어야하지만. 캐스팅 : 예. 명시 적 캐스트로 만들기에 좋은 주장입니다.
gmoody1979
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.