Обычно доменные объекты имеют свойства, которые могут быть представлены встроенным типом, но допустимые значения которых являются подмножеством значений, которые могут быть представлены этим типом.
В этих случаях значение может быть сохранено с использованием встроенного типа, но необходимо убедиться, что значения всегда проверяются в точке входа, в противном случае мы можем в итоге работать с недопустимым значением.
Одним из способов решения этой проблемы является сохранение значения как пользовательского, 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
.
Также обратите внимание, что эту структуру можно инициализировать только путем приведения из string
, но можно проверить, не удастся ли такое приведение заранее, используя IsValid
static
метод.
Казалось бы, это хороший шаблон для проверки правильности значений доменов, которые могут быть представлены простыми типами, но я не вижу, чтобы они использовались часто или предлагались, и меня интересует, почему.
Итак, мой вопрос: в чем вы видите преимущества и недостатки использования этого шаблона и почему?
Если вы чувствуете, что это плохая модель, я хотел бы понять, почему, а также то, что вы считаете лучшей альтернативой.
NB. Первоначально я задавал этот вопрос о переполнении стека, но он был отложен как основанный на мнениях (иронически субъективный сам по себе) - надеюсь, он сможет добиться большего успеха здесь.
Выше приведен исходный текст, ниже еще пара мыслей, частично в ответ на ответы, полученные до того, как он приостановился:
- Один из основных моментов, сделанных ответами, касался количества кода котельной плиты, необходимого для вышеуказанного шаблона, особенно когда требуется много таких типов. Однако в защиту паттерна это может быть в значительной степени автоматизировано с использованием шаблонов, и на самом деле мне все равно это не кажется слишком плохим, но это только мое мнение.
- С концептуальной точки зрения, не кажется ли странным при работе со строго типизированным языком, таким как C #, применять только строго типизированный принцип к составным значениям, а не распространять его на значения, которые могут быть представлены экземпляром встроенный тип?
Ответы:
Это довольно часто встречается в языках стиля ML, таких как Standard ML / OCaml / F # / Haskell, где гораздо проще создавать типы оболочек. Это дает вам два преимущества:
ValidatedName
когда-либо содержит недопустимое значение, вы знаете, что ошибка вIsValid
методе.Если вы
IsValid
правильно понимаете метод, у вас есть гарантия, что любая функция, которая получает,ValidatedName
действительно получает проверенное имя.Если вам необходимо выполнить манипуляции со строками, вы можете добавить открытый метод, который принимает функцию, которая принимает строку (значение
ValidatedName
) и возвращает строку (новое значение) и проверяет результат применения функции. Это исключает необходимость получения базового значения String и его повторной упаковки.Связанное использование для упаковки значений заключается в отслеживании их происхождения. Например, API-интерфейсы ОС на основе C иногда предоставляют дескрипторы ресурсов в виде целых чисел. Вы можете обернуть API-интерфейсы ОС, чтобы вместо этого использовать
Handle
структуру и предоставлять доступ только конструктору к этой части кода. Если код, который производитHandle
s, правильный, то когда-либо будут использоваться только допустимые дескрипторы.источник
Хорошо :
ValidatedString
делает его более понятным в отношении семантики вызова.Плохо :
IsValid
перед актером немного неудобно.ValidatedString
недействительно / проверено.Я видел такого рода вещи чаще с
User
иAuthenticatedUser
подобными вещами, где на самом деле изменяет объект. Это может быть хорошим подходом, хотя это кажется неуместным в C #.источник
Ваш путь довольно тяжелый и интенсивный. Я обычно определяю доменные объекты как:
В конструкторе сущности проверка запускается с использованием FluentValidation.NET, чтобы убедиться, что вы не можете создать сущность с недопустимым состоянием. Обратите внимание, что все свойства доступны только для чтения - вы можете установить их только через конструктор или операции с выделенным доменом.
Проверка этой сущности - это отдельный класс:
Эти валидаторы также могут быть легко использованы повторно, и вы пишете меньше стандартного кода. И еще одним преимуществом является то, что он читается.
источник
Мне нравится этот подход к типам значений. Концепция отличная, но у меня есть некоторые предложения / жалобы по поводу реализации.
Кастинг : мне не нравится использование кастинга в этом случае. Явное приведение из строки не является проблемой, но между
(ValidatedName)nameValue
новой и новой нет большой разницыValidatedName(nameValue)
. Так что это кажется ненужным. Неявное приведение к строке является худшей проблемой. Я думаю, что получение фактического строкового значения должно быть более явным, поскольку оно может быть случайно назначено строке, и компилятор не предупредит вас о возможной «потере точности». Этот вид потери точности должен быть явным.ToString : я предпочитаю использовать
ToString
перегрузки только для целей отладки. И я не думаю, что возвращать необработанное значение для этого - хорошая идея. Это та же проблема, что и при неявном преобразовании в строку. Получение внутреннего значения должно быть явной операцией. Я полагаю, что вы пытаетесь заставить структуру вести себя как обычная строка для внешнего кода, но я думаю, что при этом вы теряете часть значения, которое вы получаете от реализации такого типа.Equals и GetHashCode : структуры по умолчанию используют структурное равенство. Так что вы
Equals
иGetHashCode
дублируете это поведение по умолчанию. Вы можете удалить их, и это будет почти то же самое.источник