構造体を使用して組み込み型の検証を実施する


9

通常、ドメインオブジェクトには、組み込みの型で表すことができるプロパティがありますが、その有効な値は、その型で表すことができる値のサブセットです。

これらの場合、値は組み込み型を使用して格納できますが、値は常にエントリの時点で検証されるようにする必要があります。そうしないと、無効な値で処理される可能性があります。

これを解決する1つの方法は、組み込み型の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、これが失敗することはできませんとしてではなくfrom- stringとしてキャストexplicit、これは無効な値のためにスローされますよう、もちろんこれらの両方のことのどちらかでしたimplicitexplicit

また、からのキャストによってのみこの構造体を初期化できますstringが、IsValid staticメソッドを使用してそのようなキャストが失敗するかどうかを事前にテストできます。

これは、単純な型で表すことができるドメイン値の検証を実施するのに適したパターンのように思われますが、頻繁に使用されたり、提案されたりすることはありません。その理由について興味があります。

だから私の質問は、このパターンを使用することの利点と欠点は何だと思いますか、そしてなぜですか?

これが悪いパターンだと思われる場合は、その理由と、あなたが感じていることが最良の代替案であることを理解したいと思います。

注:この質問は当初Stack Overflowで質問しましたが、主に意見ベース(皮肉にも主観的)として保留にされました。うまくいけば、ここでより多くの成功を収めることができます。

上は元のテキストで、もう少し考えてみると、保留になる前にそこで受け取った回答に部分的に対応しています。

  • 回答で指摘された主なポイントの1つは、特にそのようなタイプが多く必要な場合に、上記のパターンに必要なボイラープレートコードの量にありました。ただし、パターンを守るために、これはテンプレートを使用して大幅に自動化することができ、実際には私にはそれほど悪くないように見えますが、それは私の意見です。
  • 概念的な観点から見ると、C#などの強く型付けされた言語を使用して、強く型付けされた原則を複合値にのみ適用するのではなく、ビルトインタイプ?

あなたはbool(T)ラムダを取るテンプレートバージョンを作ることができます
ラチェットフリーク

回答:


4

これは、標準のML / OCaml / F#/ Haskellなどのラッパータイプを作成する方がはるかに簡単なMLスタイルの言語ではかなり一般的です。次の2つの利点があります。

  • これにより、コード自体が文字列の検証を強制することができます。検証自体を処理する必要はありません。
  • これにより、検証コードを1か所にローカライズできます。ValidatedNameeverに無効な値が含まれている場合、エラーがIsValidメソッドにあることがわかります。

IsValidメソッドが正しい場合、を受け取るすべての関数がValidatedName実際に検証済みの名前を受け取っていることが保証されます。

文字列操作を行う必要がある場合は、文字列(の値ValidatedName)を受け取り、文字列(新しい値)を返し、関数を適用した結果を検証する関数を受け入れるpublicメソッドを追加できます。これにより、基になるString値を取得して再ラップするという定型がなくなります。

値をラップするための関連する用途は、その起源を追跡することです。たとえば、CベースのOS APIは、リソースのハンドルを整数として提供することがあります。OS APIをラップして、代わりにHandle構造を使用し、コードのその部分へのコンストラクターへのアクセスのみを提供できます。Handles を生成するコードが正しい場合、有効なハンドルのみが使用されます。


1

このパターンを使用するメリットとデメリットは何だと思いますか、そしてその理由は何ですか。

良い

  • それは自己完結型です。検証ビットが多すぎると、巻きひげがさまざまな場所に到達します。
  • 自己文書化に役立ちます。メソッドがa ValidatedStringを受け取るのを見ると、呼び出しのセマンティクスがより明確になります。
  • パブリックメソッド間で複製する必要がなく、検証を1つのスポットに制限するのに役立ちます。

悪い

  • キャスティングの仕掛けは隠されています。これは慣用的なC#ではないため、コードを読み取るときに混乱を招く可能性があります。
  • 投げます。検証に適合しない文字列があることは例外的なシナリオではありません。IsValidキャストの前に行うことは少し厄介です。
  • なぜ何かが無効なのかはわかりません。
  • デフォルトValidatedStringは有効/検証されていません。

私はより頻繁にこの種のものを見てきましたUserし、AuthenticatedUserオブジェクトが実際に変わるもの、のようなもの。C#では場違いのように見えますが、これはすばらしいアプローチです。


1
おかげで、4番目の "con"はそれに対する最も説得力のある引数だと思います。デフォルトまたは型の配列を使用すると、無効な値が返される可能性があります(もちろん、ゼロ/ null文字列が有効な値かどうかによって異なります)。これらは(私が思うに)無効な値になる2つの方法だけです。しかし、このパターンを使用しない場合でも、これらの2つにより無効な値が返されますが、少なくともそれらを検証する必要があることはわかっていると思います。したがって、これは潜在的な型のデフォルト値が型に対して無効であるアプローチを無効にする可能性があります。
gmoody1979、2015年

すべての短所は、概念の問題ではなく、実装の問題です。さらに、「例外は例外的である必要があります」というあいまいで不適切に定義された概念を見つけました。最も実用的なアプローチは、例外ベースのメソッドと非例外ベースのメソッドの両方を提供し、呼び出し側が選択できるようにすることです。
ドヴァル2015年

@Doval私の他のコメントに記載されている場合を除き、同意します。パターンの要点は、ValidatedNameがある場合、それが有効でなければならないことを確実に知ることです。基になるタイプのデフォルト値がドメインタイプの有効な値でもない場合、これは機能しません。これはもちろんドメインに依存しますが、数値型の場合よりも文字列ベースの型の場合がそうであると思います(私は思ったでしょう)。パターンは、基になるタイプのデフォルトがドメインタイプのデフォルトとしても適している場合に最適に機能します。
gmoody1979、2015年

@Doval-私は一般的に同意します。概念自体は問題ありませんが、洗練タイプをサポートしていない言語に効率的に絞り込んでいます。実装の問題は常にあります。
Telastyn、2015年

そうは言っても、「アウトバウンド」キャストのデフォルト値と、構造体のメソッド内の他の必要な場所でチェックし、初期化されていない場合はスローすることはできますが、乱雑になり始めます。
gmoody1979、2015年

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

これらのバリデーターは簡単に再利用することもでき、ボイラープレートコードを少なくすることができます。そしてもう一つの利点はそれが読みやすいということです。


反対投票者は、私の回答が反対投票された理由を説明する必要がありますか?
L-Four

値型を制約するための構造体に関する質問であり、WHYを説明せずにクラスに切り替えました。(反対投票ではなく、提案を行うだけです。)
DougM、2015年

これがより良い代替案である理由を説明しましたが、これは彼の質問の1つでした。返信いただきありがとうございます。
L-Four

0

値型に対するこのアプローチが好きです。コンセプトは素晴らしいですが、実装についていくつかの提案/不満があります。

キャスティング:この場合、キャスティングの使用は好きではありません。明示的なfrom-stringキャストは問題ではありませんが、(ValidatedName)nameValueとnewの間に大きな違いはありませんValidatedName(nameValue)。だからそれは一種の不必要なようです。暗黙のストリングへのキャストは最悪の問題です。実際の文字列値を取得することはより明示的である必要があると思います。誤って文字列に割り当てられる可能性があり、コンパイラーが「精度の損失」の可能性について警告しないためです。この種の精度の低下は明白です。

ToStringToStringデバッグ目的でのみオーバーロードを使用することを好みます。そして、そのための生の値を返すことは良い考えではないと思います。これは、暗黙的な文字列への変換と同じ問題です。内部値の取得は明示的な操作である必要があります。構造を外部コードに対する通常の文字列として動作させようとしていると思いますが、そうすると、この種の型を実装することで得られる値の一部が失われます。

EqualsGetHashCode:構造体はデフォルトで構造的等価性を使用しています。したがって、EqualsおよびGetHashCodeはこのデフォルトの動作を複製しています。あなたはそれらを取り除くことができ、それはほとんど同じものになります。


キャスティング:意味的に、これは、新しいValidatedNameの作成よりも、文字列からValidatedNameへの変換のように感じます。既存の文字列をValidatedNameとして識別しています。したがって、私にとってキャストは意味的にはより正しいようです。(キーボードの指の種類の)タイピングにほとんど違いがないことに同意しました。to-stringキャストに同意しません:ValidatedNameはstringのサブセットなので、精度が失われることはありません...
gmoody1979

ToString:そう思いません。私にとって、ToStringは、要件に適合していると仮定すると、デバッグシナリオ以外で使用するのに完全に有効なメソッドです。タイプが別のタイプのサブセットであるこの状況でも、機能をサブセットからスーパーセットにできる限り簡単に変換することは理にかなっていると思います。そのため、ユーザーが望めば、ほとんどのように扱うことができます。スーパーセットタイプの例、つまり文字列...
gmoody1979

EqualsとGetHashCode:はい構造体は構造的等価性を使用しますが、この例では、文字列の値ではなく文字列参照を比較しています。したがって、Equalsをオーバーライドする必要があります。基になる型が値型である場合、これは必要ないことに同意します。値型(かなり制限されています)のデフォルトのGetHashCode実装についての私の理解から、これにより同じ値が得られますが、パフォーマンスが向上します。それが事実であるかどうかを実際にテストする必要がありますが、それは質問の要点に対する副次的な問題です。ところでお返事ありがとうございます:-)
gmoody1979、2015年

@ gmoody1979構造体は、デフォルトですべてのフィールドでEqualsを使用して比較されます。文字列の問題ではないはずです。GetHashCodeと同じです。構造は文字列のサブセットであること。タイプはセーフティネットだと思っています。ValidatedNameを操作したくないのですが、誤って文字列を使用してしまいます。コンパイラーに、チェックされていないデータを処理することを明示的に指定させた方がよいでしょう。
陶酔感

申し訳ありませんが、イコールの良い点。ただし、デフォルトの動作ではリフレクションを使用して比較を行う必要があるため、オーバーライドのパフォーマンスは向上します。キャスト:はい、おそらくそれを明示的なキャストにするための良い議論です。
gmoody1979
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.