Як я б спроектував інтерфейс таким чином, щоб було зрозуміло, які властивості можуть змінювати своє значення, а які залишатись постійними?


12

У мене виникає проблема дизайну щодо властивостей .NET.

interface IX
{
    Guid Id { get; }
    bool IsInvalidated { get; }
    void Invalidate();
}

Проблема:

Цей інтерфейс має два властивості лише для читання Idта IsInvalidated. Той факт, що вони лише для читання, сам по собі не є гарантією того, що їхні значення залишаться постійними.

Скажімо, що я мав намір чітко зрозуміти, що…

  • Id являє собою постійне значення (яке може бути безпечно кешоване), а
  • IsInvalidatedможе змінити його значення протягом життя IXоб’єкта (і тому його не слід кешувати).

Як я міг змінити, interface IXщоб зробити цей договір достатньо явним?

Мої власні три спроби рішення:

  1. Інтерфейс вже добре розроблений. Наявність методу, який називається, Invalidate()дозволяє програмісту зробити висновок про те, що на нього IsInvalidatedможе впливати значення властивості аналогічного імені .

    Цей аргумент справедливий лише у випадках, коли метод та властивість мають аналогічну назву.

  2. Доповніть цей інтерфейс подією IsInvalidatedChanged:

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;

    Наявність …Changedподії для IsInvalidatedдержав говорить, що ця властивість може змінити свою цінність, а відсутність подібної події Id- це обіцянка того, що ця властивість не змінить її значення.

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

  3. Замініть властивість IsInvalidatedметодом IsInvalidated():

    bool IsInvalidated();

    Це може бути занадто тонким зміною. Це повинно бути натяком на те, що значення щоразу обчислюється щойно, що не було б необхідним, якби воно було постійним. Тема MSDN "Вибір властивостей та методів" має про це сказати:

    Використовуйте метод, а не властивість, у наступних ситуаціях. […] Операція повертає інший результат щоразу, коли вона викликається, навіть якщо параметри не змінюються.

Яких відповідей я очікую?

  • Мене найбільше цікавлять зовсім інші рішення проблеми, а також пояснення того, як вони перемогли мої вище спроби.

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

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

  • Як мінімум, я хотів би отримати зворотній зв'язок щодо того, яке саме вподобане рішення та з якої причини.


Не IsInvalidatedпотрібен private set?
Джеймс

2
@James: Не обов’язково, ні в декларації інтерфейсу, ні в реалізації:class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx

Властивості @James в інтерфейсах не такі, як автоматичні властивості, навіть якщо вони використовують один і той же синтаксис.
svick

Мені було цікаво: чи мають інтерфейси "сили" нав’язувати таку поведінку? Чи не слід цю поведінку делегувати кожній реалізації? Я думаю, що якщо ви хочете нав'язати речі до цього рівня, вам слід розглянути можливість створення абстрактного класу, щоб підтипи успадковували його, замість реалізації заданого інтерфейсу. (до речі, чи має моє обґрунтування сенс?)
heltonbiker

@heltonbiker На жаль, більшість мов (я знаю) не дозволяють виражати такі обмеження для типів, але вони, безумовно, повинні . Це питання специфікації, а не реалізації. На мій погляд, не вистачає можливості виразити інваріанти класу та пост-умову безпосередньо мовою. В ідеалі, нам ніколи не слід турбуватися про InvalidStateExceptions, наприклад, але я не впевнений, чи це навіть теоретично можливо. Було б добре, хоча.
proskor

Відповіді:


2

Я вважаю за краще рішення 3 над 1 і 2.

Моя проблема з рішенням 1 : що станеться, якщо немає Invalidateметоду? Припустимо інтерфейс із IsValidвластивістю, яка повертається DateTime.Now > MyExpirationDate; тут вам не знадобиться явний SetInvalidметод. Що робити, якщо метод впливає на кілька властивостей? Припустимо, тип з'єднання з IsOpenі IsConnectedвластивості - Closeметод впливає на обидва .

Рішення 2 : Якщо єдиним моментом події є інформування розробників про те, що аналогічне властивість може повертати різні значення при кожному виклику, то я б настійно радив проти цього. Ви повинні підтримувати інтерфейси короткими та зрозумілими; до того ж, не вся реалізація може бути здатною запустити цю подію для вас. Я повторно використаю свій IsValidприклад зверху: вам доведеться застосувати Таймер і запустити подію, коли ви дійдете MyExpirationDate. Зрештою, якщо ця подія є частиною вашого загальнодоступного інтерфейсу, користувачі інтерфейсу очікують, що ця подія спрацює.

Якщо говорити про ці рішення: вони не погані. Наявність або методу, або події БУДЕ вказувати на те, що аналогічно назване властивість може повертати різні значення при кожному виклику. Я тільки говорю, що їх одних недостатньо, щоб завжди передати це значення.

Рішення 3 - це те, про що я б ішов. Як вже було сказано в Авіві, це може працювати лише для розробників C #. Мені, як C # -dev, той факт, що IsInvalidatedне є властивістю, негайно передає значення "це не просто простий аксесуар, тут щось відбувається". Це стосується не всіх, але, як зазначила MainMa, сама структура .NET тут не узгоджується.

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

Тож моя порада буде:

Оголосіть рішення 3 конвенцією та послідовно дотримуйтесь його. У документації про властивості уточнюйте, чи має властивість змінні значення чи ні. Розробники, що працюють з простими властивостями, вірно вважають, що вони не змінюються (тобто змінюються лише тоді, коли стан об'єкта буде змінено). Розробники стикаються метод , який звучить , як це може бути властивістю ( Count(), Length(), IsOpen()) знають , що торкнутися відбувається і (сподіваюся) читати метод документи , щоб зрозуміти , що саме метод робить і як він поводиться.


Це питання отримало дуже хороші відповіді, і шкода, що я можу прийняти лише одну з них. Я вибрав вашу відповідь, тому що вона дає хороший підсумок одних питань, піднятих в інших відповідях, і тому, що це більш-менш те, що я вирішив зробити. Дякуємо всім, хто опублікував відповідь!
stakx

8

Є четверте рішення: покладатися на документацію .

Як ви знаєте, що stringклас незмінний? Ви просто знаєте це, тому що ви прочитали документацію MSDN. StringBuilderз іншого боку, є змінним, оскільки, знову ж таки, документація про це говорить.

/// <summary>
/// Represents an index of an element stored in the database.
/// </summary>
private interface IX
{
    /// <summary>
    /// Gets the immutable globally unique identifier of the index, the identifier being
    /// assigned by the database when the index is created.
    /// </summary>
    Guid Id { get; }

    /// <summary>
    /// Gets a value indicating whether the index is invalidated, which means that the
    /// indexed element is no longer valid and should be reloaded from the database. The
    /// index can be invalidated by calling the <see cref="Invalidate()" /> method.
    /// </summary>
    bool IsInvalidated { get; }

    /// <summary>
    /// Invalidates the index, without forcing the indexed element to be reloaded from the
    /// database.
    /// </summary>
    void Invalidate();
}

Ваше третє рішення непогано, але .NET Framework цього не дотримується . Наприклад:

StringBuilder.Length
DateTime.Now

є властивостями, але не сподівайтеся, що вони залишатимуться постійними щоразу.

Що постійно застосовується у самій .NET Framework:

IEnumerable<T>.Count()

є методом, тоді як:

IList<T>.Length

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


У класах просто перегляд коду чітко показує, що значення властивості не змінюватиметься протягом життя об’єкта. Наприклад,

public class Product
{
    private readonly int price;

    public Product(int price)
    {
        this.price = price;
    }

    public int Price
    {
        get
        {
            return this.price;
        }
    }
}

зрозуміло: ціна залишиться колишньою.

На жаль, ви не можете застосувати один і той же візерунок до інтерфейсу, і зважаючи на те, що C # недостатньо виразний в такому випадку, для закриття розриву слід використовувати документацію (включаючи XML-документацію та діаграми UML).


Навіть інструкція "без додаткової роботи" не використовується абсолютно послідовно. Наприклад, Lazy<T>.Valueобчислення може зайняти дуже багато часу (коли це доступ до нього вперше).
svick

1
На мою думку, не має значення, чи слід .NET Framework дотримуватися конвенції рішення 3. Я очікую, що розробники будуть досить розумні, щоб знати, чи працюють вони з внутрішніми типами або типами фреймворків. Не допоможуть і розробники, які не знають, що не читають документи, і, отже, рішення 4. Якщо рішення 3 реалізується як конвенція про компанію / команду, то я вважаю, що не має значення, чи дотримуються його й інші API (хоча, звичайно, це буде дуже вдячно).
enzi

4

Мої 2 копійки:

Варіант 3: Як не-C # -er, це не буде великим поняттям; Однак, судячи з запропонованої цитатою, досвідченим програмістам C # має бути зрозуміло, що це щось означає. Ви все ж повинні додати документи про це.

Варіант 2: Якщо ви не плануєте додавати події до кожної речі, яка може змінитися, не заходьте туди.

Інші варіанти:

  • Перейменувавши інтерфейс IXMutable, тож зрозуміло, що він може змінити свій стан. Єдине незмінне значення у вашому прикладі - це id, як правило, вважається незмінним навіть у об'єктах, що змінюються.
  • Не згадуючи про це. Інтерфейси не так добре описують поведінку, як ми хотіли б, щоб вони були; Частково, це тому, що мова насправді не дозволяє описати такі речі, як "Цей метод завжди повертає однакове значення для конкретного об'єкта", або "Виклик цього методу аргументом x <0 викине виняток".
  • За винятком того, що існує механізм розширення мови та надання більш точної інформації про конструкції - Анотації / Атрибути . Іноді вони потребують певної роботи, але дозволяють задавати довільно складні речі щодо своїх методів. Знайдіть або винайдіть примітку, яка говорить "Значення цього методу / властивості може змінюватися протягом життя об'єкта", і додайте його до методу. Можливо, вам доведеться зіграти деякі хитрощі, щоб отримати методи реалізації, щоб "успадкувати" його, і користувачам може знадобитися прочитати документ для цієї примітки, але це буде інструмент, який дозволяє вам точно сказати те, що ви хочете сказати, а не натякати на нього.

3

Іншим варіантом було б розділити інтерфейс: припускаючи, що після виклику Invalidate()кожного виклику IsInvalidatedповинно повертатися одне і те ж значення true, схоже, немає причин викликати IsInvalidatedту саму частину, на яку викликано Invalidate().

Отже, я пропоную або перевірити наявність недійсності, або ви її спричините , але робити це обидва не представляється розумним. Тому є сенс запропонувати один інтерфейс, що містить Invalidate()операцію, до першої частини, а інший інтерфейс для перевірки ( IsInvalidated) на іншу. Оскільки з підпису першого інтерфейсу досить очевидно, що це призведе до недійсності примірника, питання, що залишається (для другого інтерфейсу), справді є загальним: як визначити, чи є даний тип незмінним .

interface Invalidatable
{
    bool IsInvalidated { get; }
}

Я знаю, це не відповідає на питання безпосередньо, але принаймні це зводить до питання, як позначити якийсь тип непорушним , що є тихою загальною і зрозумілою проблемою. Наприклад, припускаючи, що всі типи є змінними, якщо не визначено інше, можна зробити висновок, що другий інтерфейс ( IsInvalidated) є змінним і, отже, може час IsInvalidatedвід часу змінювати значення . З іншого боку, припустимо, перший інтерфейс виглядав приблизно так (псевдосинтаксис):

[Immutable]
interface IX
{
    Guid Id { get; }
    void Invalidate();
}

Оскільки він позначений незмінним, ви знаєте, що Id не зміниться. Звичайно, виклик недійсного призведе до зміни стану екземпляра, але ця зміна не буде помітна через цей інтерфейс.


Не погана ідея! Однак я заперечую, що маркувати інтерфейс неправильно [Immutable], якщо він охоплює методи, єдиною метою яких є мутація. Я б перемістив Invalidate()метод в Invalidatableінтерфейс.
stakx

1
@stakx Я не згоден. :) Я думаю, що ви плутаєте поняття непорушності та чистоти (відсутність побічних ефектів). Насправді, з точки зору запропонованого інтерфейсу IX, взагалі не спостерігається мутації. Щоразу, коли ви телефонуєте Id, ви отримуватимете те саме значення кожного разу. Це ж стосується Invalidate()(ви нічого не отримуєте, оскільки його тип повернення є void). Тому тип непорушний. Звичайно, у вас виникнуть побічні ефекти , але ви не зможете спостерігати за ними через інтерфейс IX, тому вони не матимуть ніякого впливу на клієнтів IX.
proskor

Гаразд, ми можемо погодитися, що IXце незмінне. І що вони є побічними ефектами; вони просто розподіляються між двома розділеними інтерфейсами, так що клієнтам не потрібно піклуватися (у випадку IX) або, що очевидно, який може бути побічний ефект (у випадку Invalidatable). Але чому б не перейти Invalidateдо Invalidatable? Це зробило б IXнепорушним і чистим, і Invalidatableне стало б більш незмінним , ані нечистішим (бо це вже обоє).
stakx

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

1
@stakx Перш за все, ви запитали про подальші можливості якось зробити семантику вашого інтерфейсу більш явною. Моя ідея полягала в тому, щоб звести проблему до незмінності, яка вимагала розбиття інтерфейсу. Але не тільки розкол був необхідний для того, щоб зробити один інтерфейс незмінним, але це також матиме великий сенс, тому що немає підстав звертатися як до того самого споживача інтерфейсу, так Invalidate()і IsInvalidatedдо нього . Якщо ви телефонуєте Invalidate(), ви повинні знати, що він стає недійсним, то навіщо це перевіряти? Таким чином, ви можете надати два різних інтерфейси для різних цілей.
proskor
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.