Почему интерфейсы более полезны, чем суперклассы, для достижения слабой связи?

15

( Для целей этого вопроса, когда я говорю «интерфейс», я имею в виду языковую конструкциюinterface , а не «интерфейс» в другом смысле слова, то есть общедоступные методы, которые класс предлагает внешнему миру для взаимодействия и манипулировать им. )

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

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

Например, рассмотрим класс Carи два подкласса Volvoи Mazda.

Если ваш код зависит от a Car, он может использовать a Volvoили a Mazdaво время выполнения. Также позже могут быть добавлены дополнительные подклассы без необходимости изменения зависимого кода.

Кроме того, Car- что является абстракцией - с меньшей вероятностью изменится, чем Volvoили Mazda. Автомобили, как правило, были такими же в течение достаточно долгого времени, но Volvos и Mazdas гораздо чаще меняются. Т.е. абстракции более стабильны, чем конкретные типы.

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

Что я не понимаю, так это:

Абстракции могут быть суперклассами или интерфейсами.

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

Единственные различия, которые я вижу: 1. Интерфейсы не ограничены одним наследованием, но это не имеет ничего общего с темой слабой связи. 2- Интерфейсы более «абстрактны», поскольку у них вообще нет логики реализации. Но, тем не менее, я не понимаю, почему это так важно.

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

Авив Кон
источник
3
Большинство языков (например, Java, C #), которые имеют «интерфейсы», поддерживают только одиночное наследование. Поскольку у каждого класса может быть только один непосредственный суперкласс, (абстрактные) суперклассы слишком ограничены, чтобы один объект мог поддерживать несколько абстракций. Проверьте черты (например, Scala или Perl's Roles ) для современной альтернативы, которая также позволяет избежать « проблемы с алмазами » с множественным наследованием.
Амон
@amon Итак, вы говорите, что преимущество интерфейсов перед абстрактными классами при попытке получить слабую связь заключается в том, что они не ограничены одним наследованием?
Авив Кон
Нет, я имел в виду дорого с точки зрения компилятора, когда он обрабатывает абстрактный класс , он может делать больше, но этим, вероятно, можно пренебречь.
паста
2
Похоже, что @amon находится на правильном пути, я нашел этот пост, где говорится, что: interfaces are essential for single-inheritance languages like Java and C# because that's the only way in which you can aggregate different behaviors into a single class(что приводит меня к сравнению с C ++, где интерфейсы - это просто классы с чисто виртуальными функциями).
паста
Пожалуйста, скажите, кто говорит, что суперклассы плохие.
Тулаинс Кордова

Ответы:

11

Терминология: я буду ссылаться на языковую конструкцию interfaceкак на интерфейс , а на интерфейс типа или объекта как на поверхность (из-за отсутствия лучшего термина).

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

Верный.

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

Не совсем правильно. Текущие языки обычно не ожидают, что абстракция изменится (хотя есть некоторые шаблоны проектирования, чтобы справиться с этим). Отделение специфики от общих вещей - это абстракция. Обычно это делается каким-то слоем абстракции . Этот уровень можно изменить на некоторые другие особенности, не нарушая код, основанный на этой абстракции - достигается слабая связь. Пример, sortотличный от ООП: подпрограмма может быть изменена с быстрой сортировки в версии 1 на сортировку Тима в версии 2. Таким образом, код, который зависит только от сортируемого результата (т.е. основывается на sortабстракции), отделен от фактической реализации сортировки.

То, что я назвал поверхностью выше, является общей частью абстракции. Теперь в ООП случается, что один объект иногда должен поддерживать несколько абстракций. Не совсем оптимальный пример: Java java.util.LinkedListподдерживает как Listинтерфейс, который относится к абстракции «упорядоченная, индексируемая коллекция», так и поддерживает Queueинтерфейс, который (в грубом смысле) относится к абстракции «FIFO».

Как объект может поддерживать несколько абстракций?

В C ++ нет интерфейсов, но есть множественное наследование, виртуальные методы и абстрактные классы. Затем абстракция может быть определена как абстрактный класс (то есть класс, который не может быть немедленно создан), который объявляет, но не определяет виртуальные методы. Классы, которые реализуют специфику абстракции, могут затем наследоваться от этого абстрактного класса и реализовывать необходимые виртуальные методы.

Проблема в том, что множественное наследование может привести к проблеме ромба , где порядок, в котором классы ищутся для реализации метода (MRO: порядок разрешения метода), может привести к «противоречиям». Есть два ответа на это:

  1. Определите нормальный порядок и отклоните те заказы, которые не могут быть разумно линеаризованы. C3 MRO достаточно разумный и хорошо работает. Был опубликован в 1996 году.

  2. Пройдите по простому маршруту и ​​отклоните множественное наследование.

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

В результате MRO очевидно (достаточно взглянуть на каждый суперкласс по порядку), и что наш объект может иметь несколько поверхностей для любого числа абстракций.

Это оказывается довольно неудовлетворительным, потому что довольно часто немного поведения является частью поверхности. Рассмотрим Comparableинтерфейс:

interface Comparable<T> {
    public int cmp(T that);
    public boolean lt(T that);  // less than
    public boolean le(T that);  // less than or equal
    public boolean eq(T that);  // equal
    public boolean ne(T that);  // not equal
    public boolean ge(T that);  // greater than or equal
    public boolean gt(T that);  // greater than
}

Это очень удобно (хороший API с множеством удобных методов), но утомительно для реализации. Мы хотели бы, чтобы интерфейс включал cmpи реализовывал другие методы автоматически в терминах этого одного обязательного метода. Mixins , но, что более важно, Traits [ 1 ], [ 2 ] решают эту проблему, не попадая в ловушки множественного наследования.

Это делается путем определения состава признаков, так что признаки фактически не участвуют в MRO - вместо этого определенные методы объединяются в реализующий класс.

ComparableИнтерфейс может быть выражено в Scala , как

trait Comparable[T] {
    def cmp(that: T): Int
    def lt(that: T): Boolean = this.cmp(that) <  0
    def le(that: T): Boolean = this.cmp(that) <= 0
    ...
}

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

// "extends" isn't different from Java's "implements" in this case
case class Inty(val x: Int) extends Comparable[Inty] {
    override def cmp(that: Inty) = this.x - that.x
    // lt etc. get added automatically
}

Так Inty(4) cmp Inty(6)было бы -2и Inty(4) lt Inty(6)будет true.

Многие языки имеют некоторую поддержку признаков, и любой язык, который имеет «Metaobject Protocol (MOP)», может иметь добавленные признаки. В недавнем обновлении Java 8 добавлены методы по умолчанию, которые похожи на признаки (методы в интерфейсах могут иметь запасные реализации, поэтому для реализации классов необязательно реализовывать эти методы).

К сожалению, черты являются довольно недавним изобретением (2002), и, таким образом, довольно редко встречаются в более крупных основных языках.

Амон
источник
Хороший ответ, но я бы добавил, что языки с одним наследованием могут выдумывать множественное наследование, используя интерфейсы с композицией.
4

Что я не понимаю, так это:

Абстракции могут быть суперклассами или интерфейсами.

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

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

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

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

Doval
источник
Спасибо за ответ. Чтобы понять, понимаю ли я: когда вы хотите, чтобы объект с именем A зависел от абстракции с именем B, а не от конкретной реализации этой абстракции с именем C, часто лучше, чтобы B был интерфейсом, реализованным в C, а не суперклассом, расширенным C. Это потому, что: C подкласс B тесно связывает C с B. Если B изменяется - C изменяется. Однако C, реализующий B (B является интерфейсом), не связывает B с C: B - это всего лишь список методов, которые должен реализовывать C, таким образом, нет жесткой связи. Однако в отношении объекта A (зависимого) не имеет значения, является ли B классом или интерфейсом.
Авив Кон
Верный? ..... ....
Авив Кон
Почему вы считаете, что интерфейс связан с чем-либо?
Майкл Шоу,
Я думаю, что этот ответ прибивает его к голове. Я немного использую C ++, и, как было сказано в одном из других ответов, C ++ не совсем имеет интерфейсов, но вы имитируете его, используя суперклассы со всеми методами, оставленными как «чисто виртуальные» (т.е. реализованные детьми). Дело в том, что легко создавать базовые классы, которые делают что-то вместе с делегированными функциями. Во многих, многих, многих случаях я и мои коллеги обнаруживаем, что благодаря этому появляется новый сценарий использования, который лишает законной силы этот общий функционал. Если требуется общий функционал, достаточно легко создать вспомогательный класс.
J Trana
@Prog Ваша точка зрения в основном правильная, но опять же, абстракция и подтипирование - это две разные вещи. Когда вы говорите, you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named Cвы предполагаете, что классы как-то не абстрактны. Абстракция - это все, что скрывает детали реализации, поэтому класс с закрытыми полями такой же абстрактный, как интерфейс с теми же открытыми методами.
Доваль
1

Существует связь между родительским и дочерним классами, так как ребенок зависит от родителя.

Скажем, у нас есть класс A, а класс B наследуется от него. Если мы пойдем в класс A и изменим что-то, класс B тоже изменится.

Скажем, у нас есть интерфейс I, а класс B реализует его. Если мы изменим интерфейс I, то, хотя класс B может его не реализовать, класс B не изменится.

Майкл Шоу
источник
Мне любопытно, была ли причина у downvoters, или просто был плохой день.
Майкл Шоу
1
Я не понизил голос, но я думаю, что это может иметь отношение к первому предложению. Дочерние классы связаны с родительскими классами, а не наоборот. Родителю не нужно ничего знать о ребенке, но ребенку нужны глубокие знания о родителе.
@JohnGaughan: Спасибо за отзыв. Отредактировано для наглядности.
Майкл Шоу