Використання структури для примусової перевірки вбудованого типу


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 Я спочатку задав це питання на Stack Overflow, але його було зупинено як головне на основі думки (само іронічно суб'єктивне) - сподіваємось, він може тут отримати більший успіх.

Вгорі оригінальний текст, нижче ще кілька думок, частково у відповідь на відповіді, отримані там, перш ніж він затримався:

  • Одним із головних моментів відповідей був обсяг коду пластини котла, необхідний для вищезазначеної схеми, особливо коли потрібно багато таких типів. Однак на захист шаблону це може бути значною мірою автоматизовано за допомогою шаблонів, і насправді мені це все одно не здається поганим, але це лише моя думка.
  • З концептуальної точки зору, чи не здається дивним при роботі з сильно набраною мовою, такою як C #, застосовувати лише сильно набраний принцип до складених значень, а не поширювати його на значення, які можуть бути представлені екземпляром вбудований тип?

ви можете зробити шаблонну версію, яка займає лямбду bool (T)
щурячий вирод

Відповіді:


4

Це досить поширене в мовах стилю ML, таких як Standard ML / OCaml / F # / Haskell, де набагато простіше створити типи обгортки. Він надає вам дві переваги:

  • Це дозволяє фрагменту коду стверджувати, що рядок пройшов перевірку, не піклуючись про цю перевірку.
  • Це дозволяє локалізувати код перевірки в одному місці. Якщо ValidatedNameколись міститься недійсне значення, ви знаєте, що в IsValidметоді є помилка .

Якщо ви IsValidправильно отримаєте метод, ви маєте гарантію, що будь-яка функція, яка отримує ValidatedName, насправді отримує підтверджене ім’я.

Якщо вам потрібно виконати маніпуляції з рядками, ви можете додати відкритий метод, який приймає функцію, яка приймає String (значення ValidatedName) і повертає String (нове значення) і перевіряє результат застосування функції. Це виключає котельну панель отримання базового значення String і повторного загортання.

Пов'язане використання для обгортки значень полягає у відстеженні їх походження. Наприклад, API OS на основі C іноді дають ручки для ресурсів у вигляді цілих чисел. Ви можете обернути API API, щоб замість цього використовувати Handleструктуру і надавати доступ конструктору лише до тієї частини коду. Якщо код, який видає Handles, правильний, то коли-небудь використовуватимуться лише дійсні ручки.


1

які ви бачите переваги та недоліки використання цього шаблону, і чому?

Добре :

  • Це самодостатнє. Занадто багато бітів перевірки мають вусики, що потрапляють у різні місця.
  • Це допомагає самодокументації. Якщо бачити метод, це ValidatedStringробить його значно зрозумілішим щодо семантики виклику.
  • Це допомагає обмежити валідацію на одне місце, а не потребує дублювання загальнодоступних методів.

Погано :

  • Хитрість кастингу прихована. Це не ідіоматичний C #, тому може викликати плутанину при читанні коду.
  • Це кидає. Наявність рядків, які не відповідають валідації, не є винятковим сценарієм. Робити IsValidперед акторським складом трохи неприємно.
  • Він не може сказати вам, чому щось недійсне.
  • Типовий параметр ValidatedStringнедійсний / підтверджений.

Я бачив такого роду речі частіше з Userі AuthenticatedUserподібними речами, де на самому ділі змінює об'єкт. Це може бути чудовим підходом, хоча це здається непосильним у C #.


1
Дякую, я думаю, що ваш четвертий "кон" - це найпереконливіший аргумент проти нього - використання типових чи масивів типу може дати вам недійсні значення (залежно від того, нульовий / нульовий рядок є дійсним значенням курсу). Це (я думаю) єдині два способи отримати недійсне значення. Але тоді, якби ми НЕ ВИКОРИСТИЛИ цю схему, ці дві речі все-таки давали б нам недійсні значення, але я припускаю, що принаймні ми б знали, що їх потрібно перевірити. Таким чином, це може призвести до недійсного підходу, коли базове значення за замовчуванням не є дійсним для нашого типу.
gmoody1979

Усі мінуси - це питання впровадження, а не проблеми з концепцією. Крім того, я вважаю, що "винятки повинні бути винятковими" нечітка і нечітко визначена концепція. Найпрагматичніший підхід полягає у наданні як методу, що базується на винятках, так і на основі не винятку, і дозволити абоненту вибрати.
Doval

@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

Питання стосувалося структури для обмеження типів значень, і ви перейшли до класу, не пояснюючи ЧОМУ. (Не прихильник, просто внесення пропозицій.)
DougM

Я пояснив, чому вважаю це кращою альтернативою, і це було одним із його питань. Дякую за відповідь.
L-Four

0

Мені подобається такий підхід до типів цінності. Концепція чудова, але у мене є деякі пропозиції / скарги на реалізацію.

Кастинг : Мені не подобається використання кастингу в цьому випадку. Явний перехід від рядків не є проблемою, але різниці між (ValidatedName)nameValueновим та великим не існує ValidatedName(nameValue). Тож це видається непотрібним. Явна неявна передача рядків - найгірша проблема. Я думаю, що отримання фактичного значення рядка має бути більш чітким, оскільки воно може випадково присвоїтись рядку і компілятору не попередить вас про можливу "втрату точності". Цей вид втрати точності повинен бути явним.

ToString : Я вважаю за краще використовувати ToStringперевантаження тільки для налагодження. І я не думаю, що повернення сировинних цінностей для цього є гарною ідеєю. Це те саме питання, що і для неявного перетворення на рядок. Отримання внутрішнього значення повинно бути явною операцією. Я вважаю, що ви намагаєтесь змусити структуру поводитись як звичайний рядок із зовнішнім кодом, але я думаю, роблячи це, ви втрачаєте деяке значення, яке отримуєте від впровадження цього типу.

Дорівнює і GetHashCode : структури за замовчуванням використовують структурну рівність. Отже, ваша Equalsта GetHashCodeдублює цю поведінку за замовчуванням. Ви можете їх видалити, і це буде майже однакова річ.


Кастинг: Семантично це для мене більше схоже на перетворення рядка у ValidatedName, а не на створення нового ValidatedName: ми ідентифікуємо існуючий рядок як ValidatedName. Тому мені акторський склад здається більш правильним семантично. Домовились, що в наборі тексту (пальців на різноманітності клавіатури) невелика різниця. Я не погоджуюсь у передачі рядків: ValidatedName - це підмножина рядків, тому ніколи не може бути втрати точності ...
gmoody1979

ToString: Я не згоден. Для мене ToString - це абсолютно правильний метод використання поза сценаріями налагодження, якщо припустити, що він відповідає вимозі. Також у цій ситуації, коли тип - це підмножина іншого типу, я думаю, що має сенс зробити можливість перетворення з підмножини на супер-набір якомога простішим, так що, якщо бажає користувач, він може майже розцінювати це як типу
суперсети

Equals і GetHashCode: Так, структури використовують структурну рівність, але в цьому випадку, який порівнює посилання рядка, а не значення рядка. Отже, нам потрібно переборювати рівних. Я погоджуюся, що це було б не потрібно, якщо базовий тип був типом значення. З мого розуміння реалізації GetHashCode за замовчуванням для типів значень (яке досить обмежене), це дасть те саме значення, але буде більш ефективним. Я дійсно повинен перевірити, чи це так, але це трохи побічна проблема основного пункту питання. Дякую за відповідь до речі :-).
gmoody1979

@ gmoody1979 Структури порівнюються, використовуючи рівне для кожного поля за замовчуванням. Не повинно бути проблем із рядками. Те саме з GetHashCode. Що стосується структури, що є підмножиною рядка. Мені подобається вважати цей тип захисною сіткою. Я не хочу працювати з ValidatedName, а потім випадково ковзаю, щоб використовувати рядок. Я вважаю за краще, якщо компілятор змусив мене чітко вказати, що я зараз хочу працювати з неперевіреними даними.
Ейфорія

Вибачте, так, хороший бал на рівні. Хоча переоцінка має бути кращою з огляду на поведінку за замовчуванням, для порівняння потрібно використовувати відображення. Кастинг: так, можливо, хороший аргумент для того, щоб зробити це явним складом.
gmoody1979
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.