Как бы я спроектировал интерфейс так, чтобы было понятно, какие свойства могут изменить их значение, а какие останутся постоянными?

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 «Выбор между свойствами и методами» говорится следующее:

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

Какие ответы я ожидаю?

  • Меня больше всего интересуют совершенно разные решения проблемы, а также объяснение того, как они побеждают мои вышеупомянутые попытки.

  • Если мои попытки логически ошибочны или имеют существенные недостатки, о которых еще не говорилось, например, что остается только одно решение (или ни одного), я хотел бы услышать о том, где я ошибся.

    Если недостатки незначительны и после принятия во внимание остается более одного решения, оставьте комментарий.

  • По крайней мере, я хотел бы получить отзыв о том, какое решение вы предпочитаете и по какой причине.

stakx
источник
Не 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 К сожалению, большинство языков (я знаю) не позволяют выражать такие ограничения на типы, но они определенно должны это делать . Это вопрос спецификации, а не реализации. В моей Opition, что отсутствует , является возможность выразить инварианты класса и пост-условия непосредственно на языке. В идеале, нам никогда не следует беспокоиться о InvalidStateExceptions, например, но я не уверен, возможно ли это даже теоретически. Хотя было бы неплохо.
Проскор

Ответы:

2

Я бы предпочел решение 3 над 1 и 2.

Моя проблема с решением 1 : что происходит, если нет Invalidateметода? Предположим интерфейс со IsValidсвойством, которое возвращает DateTime.Now > MyExpirationDate; вам может не понадобиться явный SetInvalidметод здесь. Что если метод влияет на несколько свойств? Предположим, что тип соединения с IsOpenи IsConnectedсвойства - оба зависят от Closeметода.

Решение 2. Если единственная цель события - сообщить разработчикам, что свойство с одинаковым именем может возвращать разные значения при каждом вызове, то я настоятельно рекомендую это сделать. Вы должны держать свои интерфейсы короткими и ясными; Более того, не всякая реализация может вызвать это событие для вас. Я снова воспользуюсь своим IsValidпримером сверху: вам нужно будет реализовать Timer и запустить событие, когда вы достигнете MyExpirationDate. В конце концов, если это событие является частью вашего открытого интерфейса, то пользователи интерфейса будут ожидать, что это событие будет работать.

Это сказанное об этих решениях: они не плохи. Наличие метода или события БУДЕТ указать, что свойство с одинаковым именем может возвращать разные значения при каждом вызове. Все, что я говорю, это то, что одних их недостаточно, чтобы всегда передать это значение.

Решение 3 - это то, к чему я стремлюсь. Как уже говорил Авив, это может работать только для разработчиков на C #. Для меня, как для C #, тот факт, что 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 следовать ему или нет (хотя, конечно, это будет очень цениться).
Энзи
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();
}

Поскольку он помечен как неизменяемый, вы знаете, что идентификатор не изменится. Конечно, вызов invalidate приведет к изменению состояния экземпляра, но это изменение не будет заметно через этот интерфейс.

proskor
источник
Неплохая идея! Тем не менее, я бы сказал, что неправильно маркировать интерфейс, [Immutable]если он охватывает методы, единственная цель которых - вызвать мутацию. Я бы переместил Invalidate()метод в Invalidatableинтерфейс.
Stakx
1
@stakx Я не согласен. :) Думаю, вы путаете понятия неизменности и чистоты (отсутствие побочных эффектов). Фактически, с точки зрения предложенного интерфейса IX, наблюдаемой мутации нет вообще. Всякий раз, когда вы звоните Id, вы будете получать одно и то же значение каждый раз. То же самое относится к Invalidate()(вы ничего не получаете, так как его тип возвращаемого значения void). Следовательно, тип является неизменным. Конечно, у вас будут побочные эффекты , но вы не сможете наблюдать их через интерфейс IX, поэтому они не окажут никакого влияния на клиентов IX.
Проскор
ОК, мы можем согласиться с тем, что IXявляется неизменным. И что они являются побочными эффектами; они просто распределяются между двумя разделенными интерфейсами, так что клиентам не нужно заботиться (в случае IX) или что очевидно, каким может быть побочный эффект (в случае Invalidatable). Но почему бы не перейти Invalidateк Invalidatable? Это сделало бы IXнеизменным и чистым, и Invalidatableне стало бы более изменчивым или более нечистым (потому что это уже оба из них).
Stakx
(Я понимаю, что в реальном сценарии характер разделения интерфейса, вероятно, будет зависеть больше от случаев практического использования, чем от технических вопросов, но я пытаюсь полностью понять ваши рассуждения здесь.)
stakx
1
@stakx Прежде всего, вы спросили о дальнейших возможностях сделать семантику вашего интерфейса более явной. Моя идея заключалась в том, чтобы свести проблему к неизменности, что требовало разделения интерфейса. Но не только разделение требовалось для того, чтобы сделать один интерфейс неизменным, это также имело бы большой смысл, потому что нет причин вызывать оба Invalidate()и один IsInvalidatedи тот же потребитель интерфейса . Если вы звоните Invalidate(), вы должны знать, что он становится недействительным, так зачем это проверять? Таким образом, вы можете предоставить два разных интерфейса для разных целей.
Проскор