Подтвердить что ты не робот

Строго типизированная строка

Настройка

У меня есть прототип класса TypedString<T>, который пытается "сильно напечатать" (сомнительное значение) строки определенной категории. Он использует С# -анализ любопытно повторяющегося шаблона шаблона (CRTP).

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 во всем моем проекте, никогда не проверяя правильность форматирования имени пользователя - если у меня есть выражение или переменная типа 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, который делает немного больше, чем перенос строки, которая имеет огромное количество ограничений форматирования и вспомогательных свойств/методов для доступа к полезным частям. Так что одна часть доказательств того, что этот подход не совсем сумасшедший.

Вопрос

Я предполагаю, что это было сделано раньше. Я уже вижу преимущества этого подхода и больше не нуждаюсь в том, чтобы убедить себя.

Есть ли недостаток, который я могу потерять?
Есть ли способ, чтобы это могло вернуться, чтобы укусить меня позже?

4b9b3361

Ответ 1

Общие мысли

Я не принципиально против подхода (и не знаю, как использовать CRTP, что может быть весьма полезным). Этот подход позволяет обменивать метаданные по одному значению, что может быть очень хорошим. Он также расширяется; вы можете добавить дополнительные данные к типу без нарушения интерфейсов.

Мне не нравится тот факт, что ваша текущая реализация, похоже, сильно зависит от потока, основанного на исключении. Это может быть совершенно подходящим для некоторых вещей или в действительно исключительных случаях. Однако, если пользователь пытается выбрать правильное имя пользователя, они могут потенциально выбросить десятки исключений в процессе этого.

Конечно, вы можете добавить к интерфейсу исключение без проверки. Вы также должны спросить себя, где вы хотите, чтобы правила валидации жили (что всегда является проблемой, особенно в распределенных приложениях).

WCF

Говоря о "распределении": рассмотрим последствия реализации таких типов как часть контракта с данными WCF. Игнорируя тот факт, что контракты с данными обычно должны выставлять простые DTO, у вас также есть проблема прокси-классов, которые будут поддерживать ваши свойства типа, но не его реализацию.

Конечно, вы можете уменьшить это, разместив родительскую сборку как на клиенте, так и на сервере. В некоторых случаях это вполне уместно. В других случаях, в меньшей степени. Скажем, что для проверки одной из ваших строк требовался вызов в базу данных. Это, скорее всего, нецелесообразно иметь в обоих местах клиент/сервер.

"Сценарий 1"

Похоже, вы ищете последовательное форматирование. Это достойная цель и отлично подходит для таких вещей, как URI и, возможно, имена пользователей. Для более сложных строк это может быть проблемой. Я работал над продуктами, где даже "простые" строки можно форматировать разными способами в зависимости от контекста. В таких случаях более подходящими могут быть выделенные (и, возможно, повторно используемые) форматирующие элементы.

Опять же, очень специфичный для конкретной ситуации.

"Сценарий 2"

Хуже того, если кто-то изменит реализацию GetFriends так что он вводит ошибку и создает неверные строки имени пользователя, эта ошибка может бесшумно распространяться среди вызывающих методов и все виды хаоса.

IEnumerable<Username> GetFriends(Username user) { }

Я могу видеть этот аргумент. Приходят в голову несколько вещей:

  • Лучшее имя метода: GetUserNamesOfFriends()
  • Тестирование модулей/интеграции
  • Предположительно, эти имена пользователей проверяются при их создании/изменении. Если это ваш собственный API, почему бы вам не доверять тому, что он вам дает?

Сторона примечания: при работе с людьми/пользователями неизменяемый идентификатор, вероятно, более полезен (людям нравится изменять имена пользователей).

"Сценарий 3"

System.Uri - это пример класса .NET, который делает не что иное, как оберните строку, которая имеет огромное количество ограничений форматирования и вспомогательные свойства/методы для доступа к полезным частям. Так вот один доказательство того, что этот подход не совсем сумасшедший.

Нет аргументов, таких примеров в BCL много.

Заключительные мысли

  • Нет ничего плохого в том, чтобы обернуть значение в более сложный тип, чтобы его можно было описать/манипулировать с помощью более богатых метаданных.
  • Централизация проверки в одном месте - это хорошо, но убедитесь, что вы выбрали нужное место.
  • Пересечение границ сериализации может представлять проблемы, когда логика находится в пределах передаваемого типа.
  • Если вы в основном сосредоточены на доверии к вводу, вы можете использовать простой класс-оболочку, который позволяет узнавать, что он получает данные, которые были проверены. Не имеет значения, где/как это подтверждение произошло.

ASP.Net MVC использует аналогичную парадигму для строк. Если значение IMvcHtmlString, оно рассматривается как доверенное, а не закодированное снова. Если нет, он закодирован.

Ответ 2

Вы определили базовый класс для представления объекта того, что может быть проанализировано из строки. Сделайте все члены в базовом классе виртуальными, отличными от того, что выглядит нормально. Вы могли бы рассмотреть возможность управления сериализацией, чувствительностью к регистру и т.д. Далее.

Такое представление объекта используется в библиотеке базового класса, например System.Uri:

Uri uri = new Uri("ftp://myUrl/%2E%2E/%2E%2E");
Console.WriteLine(uri.AbsoluteUri);
Console.WriteLine(uri.PathAndQuery);

Используя этот базовый класс, просто реализовать легкий доступ к частям (например, с System.Uri), строго типизированные члены, проверку и т.д. Единственный недостаток, который я вижу, заключается в том, что множественное наследование не разрешено в С#, но вы можете не нужно наследовать какой-либо другой класс.

Ответ 3

Вот два недостатка, о которых я могу думать:

1) Разработчики обслуживания могут быть удивлены. Они также могут просто решить использовать типы CLR, а затем ваша кодовая база разделяется на код, который использует string username в некоторых местах и ​​Username username в других.

2) Ваш код может быть загроможден вызовами new Username(str) и username.Value. Теперь это может показаться не таким уж большим, но в 20 раз вы набираете username.StartsWith("a") и должны ждать, пока IntelliSense скажет вам, что что-то не так, а затем подумайте об этом, а затем исправьте его до username.Value.StartsWith("a"), вы можете раздражаться.

Я верю, что вы действительно хотите, что Ада называет "ограниченные подтипы" , но я никогда не использовал Аду. В С# лучшее, что вы можете сделать, это обертка, которая менее удобна.

Ответ 4

Я бы порекомендовал еще один дизайн.

Определите простой интерфейс, описывающий правило синтаксического анализа (строковый синтаксис):

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