Использование структуры для проверки правильности встроенного типа

9

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

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

Одним из способов решения этой проблемы является сохранение значения как пользовательского, 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 #, применять только строго типизированный принцип к составным значениям, а не распространять его на значения, которые могут быть представлены экземпляром встроенный тип?
gmoody1979
источник
вы могли бы сделать шаблонную версию , которая принимает логическую значения (T) лямбда
трещотки урод

Ответы:

4

Это довольно часто встречается в языках стиля ML, таких как Standard ML / OCaml / F # / Haskell, где гораздо проще создавать типы оболочек. Это дает вам два преимущества:

  • Он позволяет фрагменту кода обеспечить проверку того, что строка прошла проверку, не заботясь о самой проверке.
  • Это позволяет локализовать код проверки в одном месте. Если ValidatedNameкогда-либо содержит недопустимое значение, вы знаете, что ошибка в IsValidметоде.

Если вы IsValidправильно понимаете метод, у вас есть гарантия, что любая функция, которая получает, ValidatedNameдействительно получает проверенное имя.

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

Связанное использование для упаковки значений заключается в отслеживании их происхождения. Например, API-интерфейсы ОС на основе C иногда предоставляют дескрипторы ресурсов в виде целых чисел. Вы можете обернуть API-интерфейсы ОС, чтобы вместо этого использовать Handleструктуру и предоставлять доступ только конструктору к этой части кода. Если код, который производит Handles, правильный, то когда-либо будут использоваться только допустимые дескрипторы.

Doval
источник
1

В чем вы видите преимущества и недостатки использования этого шаблона и почему?

Хорошо :

  • Это автономно. Слишком много битов проверки имеют усики, проникающие в разные места.
  • Помогает самостоятельная документация. Наблюдение за методом take ValidatedStringделает его более понятным в отношении семантики вызова.
  • Это помогает ограничить валидацию одним местом, а не дублировать его в общедоступных методах.

Плохо :

  • Хитрость литья скрыта. Это не идиоматический C #, поэтому может вызвать путаницу при чтении кода.
  • Это бросает. Наличие строк, которые не соответствуют валидации, не является исключительным сценарием. Делать IsValidперед актером немного неудобно.
  • Он не может сказать вам, почему что-то не так.
  • Значение по умолчанию ValidatedStringнедействительно / проверено.

Я видел такого рода вещи чаще с Userи AuthenticatedUserподобными вещами, где на самом деле изменяет объект. Это может быть хорошим подходом, хотя это кажется неуместным в C #.

Telastyn
источник
1
Спасибо, я думаю, что ваш четвертый "con" - самый убедительный аргумент против этого - использование default или массива типа может дать вам недопустимые значения (конечно, в зависимости от того, является ли нулевая / нулевая строка допустимым значением). Это (я думаю) единственные два способа получить недопустимое значение. Но тогда, если бы мы не использовали этот шаблон, эти две вещи по-прежнему давали бы нам недопустимые значения, но я полагаю, по крайней мере, мы бы знали, что они должны быть проверены. Таким образом, это потенциально может сделать недействительным подход, в котором значение по умолчанию базового типа недопустимо для нашего типа.
gmoody1979
Все минусы - это проблемы реализации, а не проблемы с концепцией. Кроме того, я считаю, что «исключения должны быть исключительными» - нечеткая и плохо определенная концепция. Наиболее прагматичный подход - предоставить метод, основанный как на исключении, так и на исключении, и позволить вызывающей стороне выбирать.
Довал
@Doval Я согласен, за исключением случаев, отмеченных в моем другом комментарии. Суть шаблона в том, чтобы точно знать, что если у нас есть ValidatedName, он должен быть действительным. Это ломается, если значение по умолчанию базового типа также не является допустимым значением типа домена. Это, конечно, зависит от домена, но, скорее всего, будет иметь место (я бы подумал) для строковых типов, чем для числовых типов. Шаблон работает лучше всего, когда значение по умолчанию для базового типа также подходит в качестве значения по умолчанию для типа домена.
gmoody1979
@Doval - я в общем согласен. Сама концепция хороша, но она эффективно пытается включить типы уточнения в язык, который их не поддерживает. Всегда будут проблемы с реализацией.
Теластин
Сказав это, я полагаю, вы могли бы проверить значение по умолчанию для "исходящего" приведения и в любом другом необходимом месте в методах структуры и бросить, если не инициализировано, но это начинает становиться грязным.
gmoody1979
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
источник
Сможет ли даунвотер объяснить, почему мой ответ был отклонен?
L-Four
Вопрос был о структуре для ограничения типов значений, и вы переключились на класс, не объяснив, почему. (Не downvoter, просто делает предложение.)
DougM
Я объяснил, почему считаю эту альтернативу лучшей, и это был один из его вопросов. Спасибо за ответ.
L-Four
0

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

Кастинг : мне не нравится использование кастинга в этом случае. Явное приведение из строки не является проблемой, но между (ValidatedName)nameValueновой и новой нет большой разницы ValidatedName(nameValue). Так что это кажется ненужным. Неявное приведение к строке является худшей проблемой. Я думаю, что получение фактического строкового значения должно быть более явным, поскольку оно может быть случайно назначено строке, и компилятор не предупредит вас о возможной «потере точности». Этот вид потери точности должен быть явным.

ToString : я предпочитаю использовать ToStringперегрузки только для целей отладки. И я не думаю, что возвращать необработанное значение для этого - хорошая идея. Это та же проблема, что и при неявном преобразовании в строку. Получение внутреннего значения должно быть явной операцией. Я полагаю, что вы пытаетесь заставить структуру вести себя как обычная строка для внешнего кода, но я думаю, что при этом вы теряете часть значения, которое вы получаете от реализации такого типа.

Equals и GetHashCode : структуры по умолчанию используют структурное равенство. Так что вы Equalsи GetHashCodeдублируете это поведение по умолчанию. Вы можете удалить их, и это будет почти то же самое.

Euphoric
источник
Приведение: Семантически это для меня больше похоже на преобразование строки в ValidatedName, а не на создание нового ValidatedName: мы идентифицируем существующую строку как ValidatedName. Поэтому мне кажется, что актерский состав более корректен семантически. Согласитесь, есть небольшая разница в наборе (из пальцев на клавиатуре). Я не согласен с приведением к строковому приведению: ValidatedName является подмножеством строки, поэтому никогда не может быть потери точности ...
gmoody1979
ToString: я не согласен. Для меня ToString - это совершенно правильный метод, который можно использовать вне сценариев отладки, если он соответствует требованиям. Также в этой ситуации, когда тип является подмножеством другого типа, я думаю, что имеет смысл максимально упростить преобразование способности из подмножества в супернабор, чтобы, если пользователь пожелает, они могли почти рассматривать его как типа super-set, то есть строки ...
gmoody1979
Структуры Equals и GetHashCode: Yes используют структурное равенство, но в этом случае сравнивается ссылка на строку, а не ее значение. Следовательно, нам нужно переопределить Equals. Я согласен, что в этом не было бы необходимости, если бы базовый тип был типом значения. Из моего понимания реализации GetHashCode по умолчанию для типов значений (которая довольно ограничена), это даст то же значение, но будет более производительным. Я действительно должен проверить, так ли это, но это немного побочный вопрос к основному вопросу. Спасибо за ответ, кстати :-).
gmoody1979
@ gmoody1979 Структуры сравниваются с использованием равных по каждому полю по умолчанию. Не должно быть проблем со строками. То же самое с GetHashCode. Что касается структуры, являющейся подмножеством строки. Мне нравится думать о типе как о сетке безопасности. Я не хочу работать с ValidatedName, а затем случайно проскользнуть, чтобы использовать строку. Я бы предпочел, чтобы компилятор заставил меня явно указать, что теперь я хочу работать с непроверенными данными.
Эйфорическая
Извините, да, хороший момент на равных. Хотя переопределение должно работать лучше, учитывая поведение по умолчанию, для сравнения необходимо использовать отражение. Кастинг: да, возможно, хороший аргумент для того, чтобы сделать его явным.
gmoody1979