Считается ли интерфейс «пустым», если он наследуется от других интерфейсов?

12

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

Однако считается ли интерфейс «пустым», если он наследуется от других интерфейсов?

interface I1 { ... }
interface I2 { ... } //unrelated to I1

interface I3
    : I1, I2
{
    // empty body
}

Все, что реализует I3, потребуется для реализации I1и I2, и объекты из разных классов, которые наследуют, I3могут затем использоваться взаимозаменяемо (см. Ниже), так ли это правильно называть I3 пустым ? Если так, что было бы лучшим способом для архитектуры этого?

// with I3 interface
class A : I3 { ... }
class B : I3 { ... }

class Test {
    void foo() {
        I3 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function();
    }
}

// without I3 interface
class A : I1, I2 { ... }
class B : I1, I2 { ... }

class Test {
    void foo() {
        I1 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function(); // we can't do this without casting first
    }
}
Кай
источник
1
Невозможность указать несколько интерфейсов в качестве типа параметра / поля и т. Д. Является недостатком дизайна в C # / .NET.
CodesInChaos
Я думаю, что лучший способ архитектуры этой части должен идти в отдельном вопросе. Прямо сейчас принятый ответ не имеет никакого отношения к названию вопроса.
Каполь
@CodesInChaos Нет, это не так, поскольку уже есть два способа сделать это. Один из них в этом вопросе, другой способ - указать параметр общего типа для аргумента метода и использовать ограничение where для указания обоих интерфейсов.
Энди
Имеет ли смысл, что класс, который реализует I1 и I2, но не I3, не должен использоваться foo? (Если ответ «да», тогда вперед!)
user253751
@ Andy Хотя я не совсем согласен с утверждением CodesInChaos о том, что это недостаток как таковой, я не думаю, что параметры универсального типа в методе - это решение - оно возлагает ответственность за знание типа, используемого в реализации метода, на любой вызывает метод, который кажется неправильным. Возможно, какой-нибудь синтаксис, подобный var : I1, I2 something = new A();локальным переменным, был бы хорош (если мы игнорируем хорошие моменты, поднятые в ответе Конрада)
Кай

Ответы:

27

Все, что реализует I3, должно будет реализовывать I1 и I2, и объекты из разных классов, которые наследуют I3, могут затем использоваться взаимозаменяемо (см. Ниже), поэтому правильно ли называть I3 пустым? Если так, что было бы лучшим способом для архитектуры этого?

«Bundling» I1и I2into I3предлагает хороший (?) Ярлык или какой-то синтаксический сахар для того, где бы вы ни хотели реализовать оба.

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

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

void foo() {
    I1 something = new A();
    something = new B();
    something.SomeI1Function();
    something.SomeI2Function(); // we can't do this without casting first
}

ОК, ты не можешь. Но, может быть, это действительно хороший знак?

Если I1и I2подразумевать разные обязанности (иначе не было бы необходимости разделять их в первую очередь), то, может быть foo(), неправильно пытаться somethingвыполнять две разные обязанности за один раз?

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

РЕДАКТИРОВАТЬ:

Если вы настаивали на том, чтобы что- fooто реализовывало, I1и I2вы могли бы передать их как отдельные параметры:

void foo(I1 i1, I2 i2) 

И они могут быть одним и тем же объектом:

foo(something, something);

Какая fooразница, если они один и тот же экземпляр или нет?

Но если это все равно (например, потому что они не являются лицами без гражданства), или вы просто думаете, что это выглядит ужасно, можно вместо этого использовать дженерики:

void Foo<T>(T something) where T: I1, I2

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

Я воспринимаю это I3 : I2, I1как попытку эмулировать типы объединения в языке, который не поддерживает его «из коробки» (C # или Java этого не делают, тогда как типы объединения существуют в функциональных языках).

Попытка эмулировать конструкцию, которая не поддерживается языком, часто неуклюжа. Точно так же в C # вы можете использовать методы расширения и интерфейсы, если вы хотите эмулировать миксины . Возможно? Да. Хорошая идея? Я не уверен.

Конрад Моравский
источник
4

Если бы особая важность была в том, чтобы класс реализовывал оба интерфейса, то я не вижу ничего плохого в создании третьего интерфейса, который наследуется от обоих. Тем не менее, я был бы осторожен, чтобы не попасть в ловушку сверхинженерных вещей. Класс может наследовать от одного или другого, или обоих. Если у моей программы не было большого количества классов в этих трех случаях, было бы немного сложно создать интерфейс для обоих, и уж точно, если бы эти два интерфейса не имели никакого отношения друг к другу (пожалуйста, никаких интерфейсов IComparableAndSerializable).

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

Нил
источник
А? Вы, конечно, можете реализовать два интерфейса в одном классе. Вы не наследуете интерфейсы, вы реализуете их.
Энди
@ Энди Где я сказал, что один класс не может реализовать два интерфейса? Что касается использования слова «наследовать» вместо слова «реализовать», семантика. В C ++, унаследуют будет работать прекрасно , так как ближе всего к интерфейсам являются абстрактные классы.
Нил
Наследование и реализация интерфейса - это не одно и то же. Классы, наследующие несколько подклассов, могут привести к некоторым странным проблемам, чего нельзя сказать о реализации нескольких интерфейсов. А отсутствие в C ++ интерфейсов не означает, что разница исчезнет, ​​это означает, что C ++ ограничен в своих возможностях. Наследование - это отношение «есть», в то время как интерфейсы определяют «можно».
Энди
@ Andy Странные проблемы, вызванные конфликтами между методами, реализованными в указанных классах, да. Однако это уже не интерфейс ни по названию, ни на практике. Абстрактные классы, которые объявляют, но не реализуют методы, во всех смыслах этого слова, кроме имени, являются интерфейсом. Терминология меняется между языками, но это не значит, что ссылка и указатель - это не одно и то же. Это педантичный момент, но если вы настаиваете на этом, мы должны согласиться не соглашаться.
Нил
«Терминология меняется между языками» Нет, это не так. ОО терминология одинакова для разных языков; Я никогда не слышал абстрактный класс, означающий что-либо еще на каждом языке ОО, который я использовал. Да, вы можете иметь семантику интерфейса в C ++, но дело в том, что ничто не мешает кому-то идти и добавлять поведение к абстрактному классу на этом языке и нарушать эту семантику.
Энди
2

Пустые интерфейсы обычно считают плохой практикой

Я не согласен. Читайте о маркерных интерфейсах .

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

radarbob
источник
Я полагаю, что ОП знает о них, но на самом деле они немного противоречивы. Хотя некоторые авторы рекомендуют их (например, Джош Блох в "Эффективной Java"), некоторые утверждают, что они являются антипаттернами и наследием со времен, когда у Java еще не было аннотаций. (Аннотации в Java примерно эквивалентны атрибутам в .NET). У меня сложилось впечатление, что они гораздо популярнее в мире Java, чем .NET.
Конрад Моравский
1
Я использую их время от времени, но это звучит как-то хакерски - минусы, в верхней части моей головы, в том, что вы не можете помочь тому факту, что они унаследованы, поэтому, как только вы помечаете класс одним, все его дети получают отмечены также, хотите вы этого или нет. И они, кажется, поощряют плохую практику использования instanceofвместо полиморфизма
Конрад Моравски