Чому інтерфейси є більш корисними, ніж суперкласи, у досягненні вільної зв'язку?


15

( Для цього питання, коли я кажу "інтерфейс", я маю на увазі мовну конструкціюinterface , а не "інтерфейс" в іншому розумінні цього слова, тобто публічні методи, які клас пропонує зовнішньому світу для спілкування з маніпулюйте ним. )

Вільне зчеплення може бути досягнуто, якщо об'єкт залежить від абстракції замість конкретного типу.

Це дозволяє зв'язати нещільне з’єднання з двох основних причин: 1– абстракції менше змінюються, ніж конкретні типи, що означає, що залежний код менше шансів зламатися. 2–2 різні типи бетону можна використовувати під час виконання, оскільки всі вони відповідають абстракції. Нові типи бетону також можуть бути додані пізніше, не потрібно змінювати існуючий залежний код.

Наприклад, розглянемо клас Carта два підкласи Volvoта Mazda.

Якщо ваш код залежить від а Car, він може використовувати або a, Volvoабо Mazdaпід час виконання. Також пізніше додаткові підкласи можуть бути додані без необхідності змінювати залежний код.

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

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

Що я не розумію, це:

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

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

Єдині відмінності, які я бачу, полягають у тому, що: 1– Інтерфейси не обмежені єдиним успадкуванням, але це не має великого відношення до теми вільної зв'язку. 2- Інтерфейси більш "абстрактні", оскільки вони взагалі не мають логіки реалізації. Але все ж я не бачу, чому це має велику зміну.

Будь ласка, поясніть мені, чому інтерфейси, як кажуть, чудово дозволяють забезпечити вільне сполучення, тоді як прості суперкласи - ні.


3
Більшість мов (наприклад, Java, C #), які мають "інтерфейси", підтримують лише одне успадкування. Оскільки кожен клас може мати лише один негайний надклас, (абстрактні) суперкласи занадто обмежені для того, щоб один об’єкт підтримував декілька абстракцій. Ознайомтеся з ознаками (наприклад, Scala або Perl's Roles ) на сучасну альтернативу, яка також дозволяє уникнути " алмазної проблеми " з багаторазовим успадкуванням.
amon

@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 ++, де інтерфейси - це просто класи з чисто віртуальними функціями).
пастозний

Скажіть, будь ласка, хто каже, що суперкласи - це погано.
Tulains Córdova

Відповіді:


11

Термінологія: я буду називати мовну конструкцію interfaceяк інтерфейс , а інтерфейс типу або об'єкта як поверхню (за відсутності кращого терміна).

Вільне зчеплення може бути досягнуто, якщо об'єкт залежить від абстракції замість конкретного типу.

Правильно.

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

Не зовсім коректно. Поточні мови, як правило, не передбачають, що абстракція зміниться (хоча існують певні шаблони дизайну, які можуть впоратися з цим). Відокремлення специфіки від загальних речей - це абстракція. Зазвичай це робиться деяким шаром абстракції . Цей шар може бути змінений на деякі інші особливості, не порушуючи код, який спирається на цю абстракцію - досягається вільне з'єднання. Приклад, який не є OOP: sortРутину можна змінити з Quicksort у версії 1 на Tim Sort у версії 2. Код, який залежить лише від сортованого результату (тобто будується на sortабстракції), відключається від реальної реалізації сортування.

Те, що я назвав поверхнею, є загальною частиною абстракції. Зараз в OOP буває так, що один об'єкт іноді повинен підтримувати кілька абстракцій. Не зовсім оптимальний приклад: 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і реалізовував інші методи автоматично в рамках цього необхідного методу. Міксини , але, що важливіше, Риси [ 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.

Багато мов мають деяку підтримку рис, і будь-яка мова, що має "протокол метаоб'єктів (MOP)", може додавати до нього риси. Нещодавнє оновлення Java 8 додало методи за замовчуванням, схожі на ознаки (методи в інтерфейсах можуть мати резервні реалізації, так що необов'язково для класів для реалізації цих методів).

На жаль, ознаки є досить недавнім винаходом (2002), і тому є досить рідкісними для більш широких основних мов.


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

4

Що я не розумію, це:

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

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

По-перше, субтипування та абстрагування - це дві різні речі. Підтипування просто означає, що я можу замінити значення одного типу значеннями іншого типу - жоден тип не повинен бути абстрактним.

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

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


Дякую за відповідь. Щоб зрозуміти, чи я розумію: коли ви хочете, щоб об'єкт з іменем 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ви припускаєте, що заняття якимось чином не є абстрактними. Абстракція - це все, що приховує деталі реалізації, тому клас із приватними полями такий же абстрактний, як і інтерфейс з тими ж публічними методами.
Doval

1

Між класами батьків та дітей існує зв’язок, оскільки дитина залежить від батька.

Скажімо, у нас клас A, і клас B успадковує його. Якщо ми переходимо до класу A і змінюємо речі, клас B також змінюється.

Скажімо, у нас є інтерфейс I, а клас B реалізує його. Якщо ми змінимо інтерфейс I, то хоча клас B може більше не реалізовувати його, клас B не змінюється.


Мені цікаво, чи мали причину прихильники, чи просто поганий день.
Майкл Шоу

1
Я не спростовував, але, думаю, це може стосуватися першого речення. Дитячі класи поєднуються з батьківськими класами, а не навпаки. Батькові не потрібно нічого знати про дитину, але дитині потрібні інтимні знання про батька.

@JohnGaughan: Дякую за відгук. Відредаговано для наочності.
Майкл Шоу
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.