25

設定

TypedString<T>特定のカテゴリの文字列を「強く型付け」(疑わしい意味) しようとするプロトタイプ クラスがあります。これは、奇妙に繰り返されるテンプレート パターン (CRTP)の C# アナログを使用します。

class TypedString<T>

public abstract class TypedString<T>
    : IComparable<T>
    , IEquatable<T>
    where T : TypedString<T>
{
    public string Value { get; private set; }

    protected virtual StringComparison ComparisonType
    {
        get { return StringComparison.Ordinal; }
    }

    protected TypedString(string value)
    {
        if (value == null)
            throw new ArgumentNullException("value");
        this.Value = Parse(value);
    }

    //May throw FormatException
    protected virtual string Parse(string value)
    {
        return value;
    }

    public int CompareTo(T other)
    {
        return string.Compare(this.Value, other.Value, ComparisonType);
    }

    public bool Equals(T other)
    {
        return string.Equals(this.Value, other.Value, ComparisonType);
    }

    public override bool Equals(object obj)
    {
        return obj is T && Equals(obj as T);
    }

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

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

TypedString<T>プロジェクト全体でさまざまな「文字列カテゴリ」を定義するときに、このクラスを使用してコードの重複を排除できるようになりました。このクラスの簡単な使用例は、クラスを定義する場合Usernameです。

class Username(例)

public class Username : TypedString<Username>
{
    public Username(string value)
        : base(value)
    {
    }

    protected override string Parse(string value)
    {
        if (!value.Any())
            throw new FormatException("Username must contain at least one character.");
        if (!value.All(char.IsLetterOrDigit))
            throw new FormatException("Username may only contain letters and digits.");
        return value;
    }
}

これによりUsername、プロジェクト全体でクラスを使用できるようになり、ユーザー名が正しくフォーマットされているかどうかを確認する必要がなくなりました。 type の式または変数がある場合、正しい (または null) であるUsernameことが保証されます。

シナリオ 1

string GetUserRootDirectory(Username user)
{
    if (user == null)
        throw new ArgumentNullException("user");
    return Path.Combine(UsersDirectory, user.ToString());
}

ここでは、ユーザー文字列の書式設定について心配する必要はありません。型の性質上、それが正しいことは既にわかっています。

シナリオ 2

IEnumerable<Username> GetFriends(Username user)
{
    //...
}

ここで、呼び出し元は、型に基づいて戻り値として何を取得しているかを知っています。メソッドまたはドキュメントのIEnumerable<string>詳細を読む必要があります。さらに悪いことに、バグが発生して無効なユーザー名文字列が生成されるように誰かが の実装を変更した場合GetFriends、そのエラーはメソッドの呼び出し元に静かに伝播し、あらゆる種類の大混乱を引き起こす可能性があります。このきれいに型付けされたバージョンはそれを防ぎます。

シナリオ 3

System.Uri.NET のクラスの例です。これは、文字列の有用な部分にアクセスするための膨大な数の書式設定制約とヘルパー プロパティ/メソッドを持つ文字列をラップするだけです。これは、このアプローチがまったくおかしくないという証拠の 1 つです。

質問

このようなことは以前にも行われたことがあると思います。私はすでにこのアプローチの利点を理解しており、これ以上自分自身を納得させる必要はありません.

私が見逃しているかもしれない欠点はありますか?
これが後で私を噛むために戻ってくる可能性がある方法はありますか?

4

4 に答える 4

3

私が考えるデメリットは以下の2つです。

1) 保守開発者は驚かれるかもしれません。string usernameまた、CLR 型を使用することを決定するだけで、コードベースはいくつかの場所で使用するコードと他の場所で使用するコードに分割されますUsername username

2) と の呼び出しでコードが乱雑になる可能性がnew Username(str)ありusername.Valueます。これは今では大したことではないように思えるかもしれませんが、20 回目の入力username.StartsWith("a")で、IntelliSense が何かが間違っていることを通知し、それについて考えてから修正するのを待たなければならない場合、username.Value.StartsWith("a")イライラするかもしれません。

あなたが本当に望んでいるのは、Ada が「制約付きサブタイプ」と呼んでいるものだと思いますが、私自身は Ada を使用したことがありません。C# でできる最善の方法はラッパーですが、あまり便利ではありません。

于 2013-06-04T01:22:33.683 に答える
0

別のデザインをお勧めします。

解析ルール (文字列構文) を記述する単純なインターフェイスを定義します。

internal interface IParseRule
{
    bool Parse(string input, out string errorMessage);
}

ユーザー名の解析規則 (およびその他の規則) を定義します。

internal class UserName : IParseRule
{
    public bool Parse(string input, out string errorMessage)
    {
        // TODO: Do your checks here
        if (string.IsNullOrWhiteSpace(input))
        {
            errorMessage = "User name cannot be empty or consist of white space only.";
            return false;
        }
        else
        {
            errorMessage = null;
            return true;
        }
    }
}

次に、インターフェースを利用するいくつかの拡張メソッドを追加します。

internal static class ParseRule
{
    public static bool IsValid<TRule>(this string input, bool throwError = false) where TRule : IParseRule, new()
    {
        string errorMessage;
        IParseRule rule = new TRule();

        if (rule.Parse(input, out errorMessage))
        {
            return true;
        }
        else if (throwError)
        {
            throw new FormatException(errorMessage);
        }
        else
        {
            return false;
        }
    }

    public static void CheckArg<TRule>(this string input, string paramName) where TRule : IParseRule, new()
    {
        string errorMessage;
        IParseRule rule = new TRule();

        if (!rule.Parse(input, out errorMessage))
        {
            throw new ArgumentException(errorMessage, paramName);
        }
    }

    [Conditional("DEBUG")]
    public static void DebugAssert<TRule>(this string input) where TRule : IParseRule, new()
    {
        string errorMessage;
        IParseRule rule = new TRule();
        Debug.Assert(rule.Parse(input, out errorMessage), "Malformed input: " + errorMessage);
    }
}

文字列の構文を検証するクリーンなコードを記述できるようになりました。

    public void PublicApiMethod(string name)
    {
        name.CheckArg<UserName>("name");

        // TODO: Do stuff...
    }

    internal void InternalMethod(string name)
    {
        name.DebugAssert<UserName>();

        // TODO: Do stuff...
    }

    internal bool ValidateInput(string name, string email)
    {
        return name.IsValid<UserName>() && email.IsValid<Email>();
    }
于 2013-06-04T00:46:09.253 に答える