Строго типизированная строка
Настройка
У меня есть прототип класса 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, который делает немного больше, чем перенос строки, которая имеет огромное количество ограничений форматирования и вспомогательных свойств/методов для доступа к полезным частям. Так что одна часть доказательств того, что этот подход не совсем сумасшедший.
Вопрос
Я предполагаю, что это было сделано раньше. Я уже вижу преимущества этого подхода и больше не нуждаюсь в том, чтобы убедить себя.
Есть ли недостаток, который я могу потерять?
Есть ли способ, чтобы это могло вернуться, чтобы укусить меня позже?
Ответы
Ответ 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>();
}