Реализация интерфейса, когда вам не нужно одно из свойств

31

Довольно просто. Я реализую интерфейс, но есть одно свойство, которое не нужно для этого класса и, фактически, не должно использоваться. Моей первоначальной идеей было сделать что-то вроде:

int IFoo.Bar
{
    get { raise new NotImplementedException(); }
}

Я полагаю, что в этом нет ничего плохого, но это не кажется «правильным». Кто-нибудь еще сталкивался с подобной ситуацией раньше? Если да, то как ты к этому подошел?

Крис Пратт
источник
1
Я смутно припоминаю, что в C # есть какой-то полуиспользуемый класс, который реализует интерфейс, но в документации прямо говорится, что определенный метод не реализован. Я постараюсь посмотреть, смогу ли я найти это.
Маг Xy
Мне определенно было бы интересно увидеть это, если ты сможешь найти это.
Крис Пратт
23
Я могу указать несколько случаев этого в библиотеке .NET - И ОНИ ВСЕ ПРИЗНАНЫ КАК ПЛОХИЕ УЖАСНЫЕ ОШИБКИ . Это знаковое и распространенное нарушение принципа подстановки Лискова - причины, по которым не следует нарушать LSP, можно найти в моем ответе здесь
Джимми Хоффа
3
Требуется ли вам реализовать этот конкретный интерфейс, или вы могли бы ввести суперинтерфейс и использовать его?
Кристофер Шульц
6
«одно свойство, которое не нужно для этого класса» - нужна ли часть интерфейса, зависит от клиентов интерфейса, а не от разработчиков. Если класс не может разумно реализовать член интерфейса, тогда класс не подходит для интерфейса. Это может означать, что интерфейс плохо спроектирован - вероятно, пытается сделать слишком много - но это не помогает классу.
Себастьян Редл

Ответы:

51

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

  1. Возможно, класс, который вы пишете, не обеспечивает функциональность, предписанную интерфейсом, если он не использует всех членов интерфейса.
  2. Альтернативно, этот интерфейс может выполнять несколько задач и может быть разделен в соответствии с принципом разделения интерфейса.

Если первое относится к вам, просто не реализуйте интерфейс для этого класса. Думайте об этом как о электрической розетке, где отверстие для заземления не нужно, чтобы оно на самом деле не прикреплялось к земле. Вы ничего не подключаете с нуля и ничего страшного! Но как только вы воспользуетесь чем-то, что нуждается в заземлении, вас может ожидать впечатляющий провал. Лучше не пробивать ложную дыру. Поэтому, если ваш класс на самом деле не делает то, что задумал интерфейс, не реализуйте интерфейс.


Вот несколько быстрых кусочков из википедии:

Принцип замещения Лискова можно просто сформулировать так: «Не усиливайте предварительные условия и не ослабляйте постусловия».

Более формально, принцип подстановки Лискова (LSP) - это конкретное определение отношения подтипов, называемое (сильным) поведенческим подтипом, которое было впервые введено Барбарой Лисков в основном выступлении на конференции в 1987 году под названием «Абстракция и иерархия данных». Это семантическое, а не просто синтаксическое отношение, потому что оно намеревается гарантировать семантическую совместимость типов в иерархии , [...]

Для семантической совместимости и взаимозаменяемости между различными реализациями одних и тех же контрактов - вам нужно, чтобы все они придерживались одного и того же поведения.


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

Принцип сегрегации интерфейса (ISP) гласит, что ни один клиент не должен зависеть от методов, которые он не использует. [1] Интернет-провайдер разбивает очень большие интерфейсы на более мелкие и более специфичные, чтобы клиенты могли знать только о методах, которые им интересны.

Джимми Хоффа
источник
Абсолютно. Наверное, поэтому я и не почувствовал, что это правильно. Иногда вам просто нужно нежное напоминание, что вы делаете что-то глупое. Спасибо.
Крис Пратт
@ChrisPratt - это действительно распространенная ошибка, поэтому формализмы могут быть полезны - классификация запахов кода помогает быстрее идентифицировать их и вызывать ранее использованные решения.
Джимми Хоффа
4

Выглядит хорошо для меня, если это ваша ситуация.

Однако мне кажется, что ваш интерфейс (или его использование) не работает, если производный класс на самом деле не реализует все это. Попробуйте разделить этот интерфейс.

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

Гонки легкости с Моникой
источник
6
C # не поддерживает множественное наследование классов, но поддерживает интерфейсы.
mgw854 29.12.15
2
Я думаю ты прав. В конце концов, интерфейс - это контракт, и даже если я знаю, что этот конкретный класс не будет использоваться таким образом, чтобы он ломал все, что использует интерфейс, если это свойство отключено, это не очевидно.
Крис Пратт
@ChrisPratt: Да.
Легкость гонок с Моникой
4

Я сталкивался с этой ситуацией. На самом деле, как указано в другом месте, в BCL есть такие примеры ... Я попытаюсь привести лучшие примеры и дать некоторое обоснование:

Если у вас уже есть поставляемый интерфейс, который вы сохраняете по причинам совместимости и ...

  • Интерфейс содержит элементы, которые являются устаревшими или устаревшими. Например BlockingCollection<T>.ICollection.SyncRoot(среди прочего), хотя ICollection.SyncRootи не устарел как таковой, он бросит NotSupportedException.

  • Интерфейс содержит элементы, которые задокументированы как необязательные, и что реализация может выдать исключение. Например, в MSDN по IEnumerator.Resetэтому поводу сказано:

Метод Reset предназначен для взаимодействия COM. Это не обязательно должно быть реализовано; вместо этого разработчик может просто вызвать исключение NotSupportedException.

  • По ошибке дизайна интерфейса, это должно было быть больше чем один интерфейс во-первых. В BCL распространена схема реализации версий контейнеров только для чтения NotSupportedException. Я сделал это сам, это то, что ожидается сейчас ... Я ICollection<T>.IsReadOnlyвозвращаюсь, trueчтобы вы могли сказать им аппарта. Правильный дизайн должен был бы иметь читаемую версию интерфейса, и тогда полный интерфейс наследуется от этого.

  • Нет лучшего интерфейса для использования. Например, у меня есть класс, который позволяет вам получить доступ к элементам по индексу, проверить, содержит ли он элемент и по какому индексу, имеет ли он какой-либо размер, вы можете скопировать его в массив ... это кажется работой, IList<T>но мой класс имеет фиксированный размер и не поддерживает ни добавление, ни удаление, поэтому он работает больше как массив, чем список. Но нет IArray<T>в BCL .

  • Интерфейс принадлежит API, который портирован на несколько платформ, и при реализации конкретной платформы некоторые его части не поддерживаются. В идеале должен быть какой-то способ его обнаружения, чтобы переносимый код, использующий такой API, мог решать, как называть эти части, или нет, но если вы их вызываете, это абсолютно уместно NotSupportedException. Это особенно верно, если это порт для новой платформы, которая не была предусмотрена в оригинальном дизайне.


Также подумайте, почему это не поддерживается?

Иногда InvalidOperationExceptionэто лучший вариант. Например, еще один способ добавить полиморфизм в класс - это иметь различную реализацию внутреннего интерфейса, и ваш код выбирает, какой из них создать, в зависимости от параметров, заданных в конструкторе класса. [Это особенно полезно, если вы знаете, что набор опций фиксирован, и вы не хотите, чтобы сторонние классы вводились путем внедрения зависимостей.] Я сделал это для обратного переноса ThreadLocal, потому что реализация отслеживания и не отслеживания слишком далеко appart, и что ThreadLocal.Valuesбросает на реализацию без отслеживания?InvalidOperationExceptionдаже если это не зависит от состояния объекта. В этом случае я ввел класс сам и знал, что этот метод должен быть реализован просто с помощью исключения.

Иногда значение по умолчанию имеет смысл. Например, в ICollection<T>.IsReadOnlyупомянутом выше смысле имеет смысл просто возвращать «true» или «false» в зависимости от случая. Итак ... какова семантика IFoo.Bar? там может быть какое-то разумное значение по умолчанию для возврата.


Приложение: если вы контролируете интерфейс (и вам не нужно оставаться с ним для совместимости), не должно быть случая, когда вы должны бросить NotSupportedException. Хотя вам, возможно, придется разделить интерфейс на два или более меньших интерфейса, чтобы они подходили для вашего случая, что может привести к «загрязнению» в экстремальных ситуациях.

Theraot
источник
0

Кто-нибудь еще сталкивался с подобной ситуацией раньше?

Да, разработчики библиотеки .Net сделали. Класс Dictionary делает то, что вы делаете. Он использует явную реализацию, чтобы эффективно скрыть * некоторые методы IDictionary. Здесь это объясняется лучше , но в итоге, чтобы использовать методы словаря Add, CopyTo или Remove, которые принимают KeyValuePairs, сначала необходимо привести объект к IDictionary.

* Это не «сокрытие» методов в строгом смысле слова «скрыть», как это использует Microsoft . Но я не знаю лучшего термина в этом случае.

user2023861
источник
... что ужасно
Легкость гонки с Моникой
Почему отрицательный голос?
user2023861 29.12.15
2
Возможно, потому что вы не ответили на вопрос. Иш. Вроде.
Легкость гонок с Моникой
@LightnessRacesinOrbit, я обновил свой ответ, чтобы сделать его более понятным. Я надеюсь, что это помогает ОП.
user2023861 29.12.15
2
Похоже, это отвечает на вопрос для меня. Тот факт, что это может быть хорошей или плохой идеей, кажется, не входит в суть вопроса, но все же может быть полезным для обозначения ответов.
Элледедил
-6

Вы всегда можете просто реализовать и вернуть мягкое значение или значение по умолчанию. Ведь это просто собственность. Может возвращать 0 (свойство по умолчанию для int) или любое другое значение, имеющее смысл в вашей реализации (int.MinValue, int.MaxValue, 1, 42 и т. Д.)

//We don't officially implement this property
int IFoo.Bar
{
     { get; }
}

Бросок исключения кажется плохой формой.

Джон Рейнор
источник
2
Зачем? Как лучше возвращать неверные данные?
Легкость гонок с Моникой
1
Это нарушает LSP: см. Ответ Джимми Хоффа для объяснения причин.
5
Если нет возможных правильных возвращаемых значений, лучше создать исключение, чем возвращать неправильное значение. Независимо от того, что вы делаете, ваша программа будет работать со сбоями, если она случайно вызовет это свойство. Но если вы выбросите исключение, то станет понятно, почему оно работает неправильно.
Таннер Светт
Я не знаю, как значение будет неправильным, когда вы реализуете интерфейс! Это ваша реализация, она может быть чем угодно.
Джон Рейнор
Что, если правильное значение было неизвестно во время компиляции?
Энди