Чи є якась "реальна" причина, коли багаторазове спадкування ненавидять?


123

Мені завжди подобалася думка про підтримку декількох успадкованих мов. Найчастіше, хоча це навмисно пропало, і передбачувана "заміна" - це інтерфейси. Інтерфейси просто не охоплюють все одне й те саме багаторазове успадкування, і це обмеження час від часу може призвести до збільшення кодового коду.

Єдина основна причина, про яку я коли-небудь чув про це - проблема алмазів із базовими класами. Я просто не можу цього прийняти. Для мене це виходить дуже жахливо, як-от: "Ну, це можна викрутити, тому це автоматично погана ідея". Хоча ви можете накрутити що-небудь мовою програмування, і я маю на увазі все, що завгодно. Я просто не можу сприймати це серйозно, принаймні, не без більш ретельного пояснення.

Тільки усвідомлення цієї проблеми - це 90% битви. Крім того, я думаю, що я чув що-небудь років тому про загальні цілі роботи, що включають алгоритм "конверт" або щось подібне (чи цей дзвінок дзвонить, хтось?).

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

Так що всі сказані ... Чи є якась реальна причина, що більшість людей ненавидять багаторазове спадкування, чи це все лише купа істерики, яка завдає більше шкоди, ніж користі? Чи є щось таке, чого я тут не бачу? Дякую.

Приклад

Автомобіль розширює WheeledVehicle, KIASpectra розширює Car and Electronic, KIASpectra містить радіо. Чому KIASpectra не містить електронних?

  1. Тому що це Електронний. Спадкове відношення до складу завжди має бути співвідношенням "відносини" проти "відносини".

  2. Тому що це Електронний. Є дроти, плати, вимикачі тощо, все це вгору і вниз.

  3. Тому що це Електронний. Якщо акумулятор взимку загине, у вас виникають такі ж проблеми, як якщо б усі ваші колеса раптово зникли.

Чому б не використовувати інтерфейси? Візьмемо, наприклад, №3. Я не хочу писати це знову і знову, і я дійсно не хочу створювати якийсь химерний клас помічників проксі для цього:

private void runOrDont()
{
    if (this.battery)
    {
        if (this.battery.working && this.switchedOn)
        {
            this.run();
            return;
        }
    }
    this.dontRun();
}

(Ми не вникаємо в те, добре чи погано це реалізація.) Ви можете уявити, як може бути кілька цих функцій, пов’язаних з Electronic, які не пов'язані ні з чим у WheeledVehicle, і навпаки.

Я не був впевнений, чи слід влаштовуватися на тому прикладі чи ні, оскільки тут є місце для інтерпретації. Ви також можете подумати з точки зору розширення літака транспортного засобу та FlyingObject та Bird Animal та FlyingObject або з точки зору набагато більш чистого прикладу.


24
це також заохочує спадщину над складом ... (коли це має бути навпаки)
храповик виродка

34
"Ви можете це викрутити" - це абсолютно вагома причина для видалення функції. Сучасний мовний дизайн дещо розщеплений з цього приводу, але ви, як правило, можна класифікувати мови на "Влада від обмеження" та "Влада від гнучкості". Жодне з яких не є правильним, я не думаю, що вони мають сильні моменти. ІМ - одна з тих речей, яка найчастіше використовується для Зла, і, таким чином, обмежувальні мови знімають його. Гнучких немає, тому що "найчастіше" не "буквально завжди". Проте, міксини / ознаки є кращим рішенням для загального випадку.
Фоши

8
Існують більш безпечні альтернативи багатократному успадкуванню на декількох мовах, які виходять за межі інтерфейсів. Ознайомтеся зі стандартами Scala Traits- вони діють як інтерфейси з необов'язковою реалізацією, але мають деякі обмеження, які допомагають запобігти виникненню проблем, таких як алмазна проблема.
KChaloux

5
Дивіться також це раніше закрите запитання: programmers.stackexchange.com/questions/122480/…
Doc Brown

19
KiaSpectraне є Electronic ; у нього є Електроніка, і може бути ElectronicCar(що розшириться Car...)
Brian S

Відповіді:


68

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

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

Деякі мови вирішили зробити риси явною конструкцією в межах мови. Інші обережно направляють вас у цьому напрямку, видаляючи ІМ з мови. Так чи інакше, я не можу придумати жодного випадку, коли я подумав: "Людина, мені справді потрібен ІМ, щоб це зробити правильно".

Також обговоримо, що насправді насправді є спадщиною. Коли ви успадковуєте клас, ви приймаєте залежність від цього класу, але також вам потрібно підтримувати контракти, які підтримує клас, як неявні, так і явні.

Візьмемо класичний приклад квадрата, що успадковується від прямокутника. Прямокутник виявляє властивість довжини та ширини, а також метод getPerimeter та getArea. Квадрат змінює довжину і ширину, щоб при встановленні одного встановити інший, щоб він відповідав getPerimeter і getArea працював би однаково (2 * довжина + 2 * ширина по периметру і довжина * ширина для області).

Є один тестовий випадок, який розривається, якщо замінити цю реалізацію квадрата прямокутником.

var rectangle = new Square();
rectangle.length= 5;
rectangle.width= 6;
Assert.AreEqual(30, rectangle.GetArea()); 
//Square returns 36 because setting the width clobbers the length

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


Підводні камені, про які я згадував разом із Пегасом у MI та відносинами Прямокутник / Квадрат - це результати недосвідченого дизайну для занять. В основному уникнення багаторазового успадкування - це спосіб допомогти початківцям розробникам уникати стрілянини в ногу. Як і всі принципи дизайну, дисциплінованість та навчання на їх основі дозволяє вчасно виявити, коли з них нормально відірватися. Дивіться Модель Дрейфуса набуття навичок на рівні Експертів, ваші внутрішні знання перевертають залежність від сентенцій / принципів. Ви можете "відчути", коли правило не застосовується.

І я погоджуюся, що я дещо обдурив приклад "реального світу", чому МІ нахмурився.

Давайте розглянемо рамки інтерфейсу користувача. Зокрема, давайте розглянемо кілька віджетів, які, можливо, спочатку кисть виглядають так, що вони є просто комбінацією двох інших. Як ComboBox. ComboBox - це TextBox, який підтримує DropDownList. Тобто я можу ввести значення або можу вибрати з попередньо визначеного списку значень. Наївним підходом було б успадкування ComboBox від TextBox та DropDownList.

Але ваш Textbox отримує свою цінність від того, що ввів користувач. У той час як DDL отримує свою цінність від того, що вибирає користувач. Хто приймає прецедент? DDL, можливо, був розроблений для перевірки та відхилення будь-яких вхідних даних, які не були в його первісному списку значень. Чи перекриваємо ми цю логіку? Це означає, що ми маємо викрити внутрішню логіку для того, щоб спадкоємці перекрили. Або ще гірше, додайте логіку до базового класу, який є лише для того, щоб підтримувати підклас (що порушує принцип інверсії залежності ).

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

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

Розробники поглибили цю концепцію далі і використовували додані властивості як спосіб поведінки компонентів (наприклад, ось моя публікація щодо сортування сортування GridView з використанням доданих властивостей, написаних до того, як WPF включив DataGrid). Цей підхід був визнаний як модель дизайну XAML під назвою Attached Behaviors .

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


14
"Людина, мені справді потрібен ІМ, щоб це зробити правильно", - дивіться мою відповідь на приклад У двох словах, ортогональні поняття, які задовольняють співвідношення "є", мають сенс для ІМ. Вони дуже рідкісні, але вони існують.
MrFox

13
Людина, я ненавиджу приклад квадратного прямокутника. Існує маса більш яскравих прикладів, які не потребують змін.
asmeurer

9
Мені подобається, що відповідь "надати справжній приклад" полягала в обговоренні вигаданої істоти. Відмінна іронія +1
jjathman

3
Отже, ви говорите, що багаторазове успадкування - це погано, оскільки успадкування - це погано (ви є прикладами про успадкування одного інтерфейсу). Тому, грунтуючись лише на цій відповіді, мова повинна або позбавлятись успадкування та інтерфейсів, або мати багаторазове успадкування.
ctrl-alt-delor

12
Я не думаю, що ці приклади справді спрацьовують ... ніхто не каже, що пегас - це птах і кінь ... ви кажете, що це Крилатий Анімал і Копита-Анімаль. Приклад квадрата та прямокутника має трохи більше сенсу, тому що ви опиняєтесь з об’єктом, який веде себе інакше, ніж ви могли б подумати, грунтуючись на його затвердженому визначенні. Однак я думаю, що ця ситуація була б виною програмістів у тому, щоб цього не домогтися (якщо це справді виявилося б проблемою).
Річард

60

Чи є щось таке, чого я тут не бачу?

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

Ще один поширений аргумент, який я бачив (і робив часом), полягає в тому, що, маючи два + базові класи, ваш об'єкт майже незмінно порушує Принцип єдиної відповідальності. Або два базові класи - це добрі автономні класи з власною відповідальністю (спричиняючи порушення), або вони є частковими / абстрактними типами, які працюють один з одним, щоб скласти єдину згуртовану відповідальність.

У цьому іншому випадку у вас є 3 сценарії:

  1. Не знає нічого про B - Чудово, ви могли поєднувати класи, тому що вам пощастило.
  2. А знає про B - Чому А просто не успадкував від B?
  3. A і B знають один про одного - чому ви просто не склали один клас? Яка користь від того, щоб зробити ці речі такими сполученими, але частково замінюваними?

Особисто я вважаю, що багатократне успадкування має поганий реп, і що добре зроблена система композиції стилю риси була б дійсно потужною / корисною ... але є багато способів, як це можна погано реалізувати, і це багато причин не гарна ідея такою мовою, як C ++.

[редагувати] щодо вашого прикладу, це абсурд. У Kia є електроніка. У нього є двигун. Так само електроніка має джерело живлення, яке, як правило, є автомобільним акумулятором. Спадщині, не кажучи вже про багаторазове успадкування там немає місця.


8
Ще одне цікаве питання - як ініціалізувати базові класи + їх базові класи. Інкапсуляція> Спадщина.
jozefg

«Добре зроблено система ознаки складу стилю буде дійсно потужною / корисної ...» - Це відомо як Mixins , і вони є потужними / корисними. Міксини можуть бути реалізовані за допомогою ІМ, але для отримання сумішей не потрібно багатократне успадкування. Деякі мови підтримують міксини по своїй суті, без ІМ.
BlueRaja - Danny Pflughoeft

Зокрема, для Java у нас вже є декілька інтерфейсів та INVOKEINTERFACE, тому MI не матиме явних наслідків для продуктивності.
Даніель Любаров

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

29

Єдина причина, чому це заборонено, це тому, що це дозволяє людям легко стріляти в ногу.

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

Якщо ваші розробники відчувають себе в Інтернеті, і ІМ має сенс в контексті того, що ви робите, то ви сильно пропустите це мовою, яка його не підтримує. У той же час, якщо колективу це не комфортно, або немає в ньому реальної потреби, і люди використовують його "лише тому, що можуть", то це є контрпродуктивним.

Але ні, не існує всепереконливого абсолютно правдивого аргументу, який доводить, що багаторазове успадкування є поганою ідеєю.

EDIT

Відповіді на це питання здаються одностайними. Заради того, щоб бути захисником диявола, я надам хороший приклад багаторазового успадкування, коли не робити цього призводить до злому.

Припустимо, ви розробляєте додаток на ринках капіталу. Вам потрібна модель даних про цінні папери. Деякі цінні папери є продуктами власного капіталу (акції, інвестиційні фонди тощо), інші - боргові (облігації, корпоративні облігації), інші - деривативи (опціони, ф'ючерси). Отже, якщо ви уникаєте MI, ви складете дуже чітке, просте дерево спадкування. Акція успадкує власний капітал, облігація успадкує борг. Чудовий поки що, а як щодо похідних? Вони можуть базуватися на продуктах, що нагадують власний капітал, або на дебетових продуктах? Гаразд, я думаю, ми зробимо наше гілкове дерево спадкування більше. Майте на увазі, що деякі деривативи базуються на власних продуктах, боргових продуктах, або жодних. Тож наше спадкове дерево ускладнюється. Потім приходить бізнес-аналітик і каже вам, що зараз ми підтримуємо індексовані цінні папери (опції індексу, майбутні опціони). І ці речі можуть базуватися на капіталі, боргу чи похідних. Це стає безладно! Чи походить мій майбутній варіант індексу власного капіталу-> акції-> опції-> індексу? Чому б не власний капітал-> Фондовий-> індекс-> Варіант? Що робити, якщо одного разу я знайду обоє у своєму коді (Це сталося; правдива історія)?

Проблема тут полягає в тому, що ці основні типи можуть бути змішані в будь-якій перестановці, яка природно не виходить одна з іншої. Об'єкти визначаються по є залежністю, тому композиція не має ніякого сенсу. Множинне успадкування (або аналогічне поняття міксин) - єдине логічне уявлення тут.

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


27
Я працював у фінансах. Існує причина, чому функціональне програмування віддається перевазі OO у фінансовому програмному забезпеченні. І ви це вказали. ІМ не вирішує цю проблему, вона її посилює. Повірте, я не можу сказати вам, скільки разів я чув "Це просто так ... за винятком", коли я маю справу з клієнтами, що займаються фінансами.
Майкл Браун

8
Це здається мені дуже вирішуваним із інтерфейсами ... насправді, це здається, що краще підходить для інтерфейсів, ніж будь-що інше. Equityі Debtобидва реалізують ISecurity. Derivativeмає ISecurityвластивість. Це може бути, ISecurityякщо це доречно (я не знаю фінансів). IndexedSecuritiesзнову містить властивість інтерфейсу, яка застосовується до типів, на яких дозволяється базуватися. Якщо вони всі ISecurity, то всі вони мають ISecurityвластивості і можуть бути довільно вкладені ...
Бобсон

11
@Bobson проблема полягає в тому, що вам доведеться повторно реалізовувати інтерфейс для кожного реалізатора, і в багатьох випадках це однаково. Ви можете використовувати делегацію / склад, але тоді втрачаєте доступ до приватних членів. А оскільки у Java немає делегатів / лямбда ..., це практично не існує. За винятком можливого інструменту Aspect, як Aspect4J
Michael Brown

10
@Bobson саме те, що сказав Майк Браун. Так, ви можете спроектувати рішення з інтерфейсами, але це буде незграбно. Ваша інтуїція використовувати інтерфейси дуже коректна, але це приховане бажання змішання / багаторазового успадкування :).
MrFox

11
Це стосується дивацтва, коли програмісти OO вважають за краще успадковувати і часто не сприймають делегування як дійсний підхід до ОО, який, як правило, дає більш мізерне, більш реконструйоване та розширене рішення. Попрацювавши у сфері фінансів та охорони здоров’я, я бачив, що мінливий безлад може створити, особливо, коли податкове та законодавство про охорону здоров’я змінюється з кожним роком, а визначення об'єкта ОСТАННІЙ рік недійсне для Цього року (все ж повинно виконувати функцію будь-якого року , і повинні бути взаємозамінними). Склад дає простіший код, який легше перевірити, і він коштує менше з часом.
Shaun Wilson

11

Інші відповіді тут, здається, отримують здебільшого теоретичні. Отже, ось конкретний приклад Python, спрощений вниз, що я насправді врізався в голову, що вимагало неабиякої кількості рефакторингу:

class Foo(object):
   def zeta(self):
      print "foozeta"

class Bar(object):
   def zeta(self):
      print "barzeta"

   def barstuff(self):
      print "barstuff"
      self.zeta()

class Bang(Foo, Bar):
   def stuff(self):
      self.zeta()
      print "---"
      self.barstuff()

z = Bang()
z.stuff()

Barбув написаний, припускаючи, що він мав власну реалізацію zeta(), що, як правило, є досить хорошим припущенням. Підклас повинен заміняти його, як це доречно, щоб він зробив правильно. На жаль, імена були лише випадково однаковими - вони робили досить різні речі, але Barтепер закликали Fooреалізацію:

foozeta
---
barstuff
foozeta

Це досить засмучує, коли немає помилок, додаток починає діяти так просто неправильно, і зміна коду, що викликала його (створення Bar.zeta), здається, не там, де лежить проблема.


Як уникнути проблеми з Diamond за допомогою дзвінків до super()?
Panzercrisis

@Panzercrisis Вибачте, ніколи не забудьте про цю частину. Я помилково пригадав, до якої проблеми зазвичай відноситься "алмазна проблема" - у Python з'явилася окрема проблема, спричинена спадщиною у формі ромба, яка super()виникла
Izkata

5
Я радий, що MI + у C ++ значно краще розроблений. Вищезгадана помилка просто неможлива. Ви повинні вручну відключити його, перш ніж код навіть скомпілюватиметься.
Томас Едінг

2
Зміна порядку успадкування в Bangна Bar, Fooтакож вирішить проблему - але ваш пункт хороший, +1.
Шон Віейра

1
@ThomasEding Ви можете вручну неоднозначність ще: Bar.zeta(self).
Тайлер Кромптон

9

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

Я роблю це в Гуаві , мові, над якою працюю. Однією з особливостей Guava є те, що ми можемо викликати реалізацію певного супертипу методу. Тож легко вказати, яку реалізацію супертипу слід "успадкувати", без особливих синтаксисів:

type Sequence[+A] {
  String toString() {
    return "[" + ... + "]";
  }
}

type Set[+A] {
  String toString() {
    return "{" + ... + "}";
  }
}

type OrderedSet[+A] extends Sequence[A], Set[A] {
  String toString() {
    // This is Guava's syntax for statically invoking instance methods
    return Set.toString(this);
  }
}

Якби ми не дали OrderedSetцього toString, ми отримали б помилку компіляції. Ніяких сюрпризів.

Я вважаю MI особливо корисним у колекціях. Наприклад, мені подобається використовувати RandomlyEnumerableSequenceтип, щоб уникнути декларування getEnumeratorдля масивів, декетів тощо:

type Enumerable[+A] {
  Source[A] getEnumerator();
}

type Sequence[+A] extends Enumerable[A] {
  A get(Int index);
}

type RandomlyEnumerableSequence[+A] extends Sequence[A] {
  Source[A] getEnumerator() {
    ...
  }
}

type DynamicArray[A] extends MutableStack[A],
                             RandomlyEnumerableSequence[A] {
  // No need to define getEnumerator.
}

Якщо у нас не було MI, ми могли б написати атрибути RandomAccessEnumeratorдля декількох колекцій, але необхідність написання короткого getEnumeratorметоду все ще додає котельну табличку.

Аналогічно, MI корисний як для успадкування стандартних реалізацій equals, так hashCodeі toStringдля колекцій.


1
@AndyzSmith, я бачу, що ви розумієте, але не всі конфліктні спадщини є актуальними семантичними проблемами. Розглянемо мій приклад - жоден із типів не обіцяє toStringповедінку, тому переосмислення її поведінки не порушує принцип заміни. Також іноді виникають "конфлікти" між методами, які мають однакову поведінку, але різні алгоритми, особливо з колекціями.
Даніель Любаров

1
@Asmageddon погодився, мої приклади включають лише функціональність. Які ви бачите переваги міксинів? Принаймні, у Scala, риси, по суті, є лише абстрактними класами, які дозволяють МІ - вони все ще можуть бути використані для ідентичності та все ще викликають проблему з алмазами. Чи існують інші мови, у яких у мікінів є більш цікаві відмінності?
Даніель Любаров

1
Технічно абстрактні класи можна використовувати як інтерфейси, для мене вигода просто тим, що сама конструкція є "підказкою щодо використання", тобто я знаю, чого чекати, коли я бачу код. Однак, в даний час я кодую в Lua, яка не має властивої моделі OOP - існуючі класи класів зазвичай дозволяють включати таблиці як міксини, а той, який я кодую, використовує класи як міксини. Єдина відмінність полягає в тому, що ви можете використовувати лише (єдине) успадкування для ідентичності, суміші для функціональності (без інформації про особу) та інтерфейси для подальшої перевірки. Можливо, це не якось краще, але для мене це має сенс.
Llamageddon

2
Чому ви називаєте Ordered extends Set and Sequencea Diamond? Це просто приєднання. Не вистачає hatвершини. Чому ви називаєте це діамантом? Я запитав тут, але це здається табуйованим питанням. Звідки ви знали, що вам потрібно називати цю трикутну структуру Diamond, а не приєднуватися?
Val

1
@RecognizeEvilasWaste, що мова має неявний Topтип, який декларує toString, тож насправді була алмазна структура. Але я думаю, у вас є сенс - "з'єднання" без алмазної структури створює подібну проблему, і більшість мов обидва випадки поводяться однаково.
Даніель Любаров

7

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

Зв'язаний список і рядок символів мають мало спільного, і їх не потрібно пов'язувати по спадку, але це корисно, якщо я можу використовувати lengthфункцію, щоб отримати кількість елементів у будь-якому одному.

Спадкування - це хитрість уникнути повторної реалізації коду. Якщо спадкування заощаджує вашу роботу, а багатократне спадкування економить вам ще більше роботи порівняно з одинарним спадкуванням, то це все необхідне виправдання.

Я підозрюю, що деякі мови не реалізують багатократне успадкування, а також для практикуючих цих мов. Це означає, що багаторазове успадкування. Згадайте про багаторазове успадкування програміста на C ++, і те, що вам спадає на думку, - це питання, коли клас закінчується двома копіями бази за допомогою двох різних шляхів успадкування, а також про те, чи слід використовувати virtualв базовому класі, і плутанина щодо того, як називають деструктори , і так далі.

У багатьох мовах успадкування класу пов'язане з успадкуванням символів. Коли ви отримуєте клас D з класу B, ви не тільки створюєте відносини типу, але оскільки ці класи також виконують роль лексичних просторів імен, ви маєте справу з імпортом символів з простору імен B до простору імен D, на додаток до семантика того, що відбувається з самими типами B і D. Тому множинне успадкування викликає питання зіткнення символів. Якщо ми успадковуємо card_deckі graphic, і той і інший drawметод "має" , що це означає для drawотриманого об'єкта? Об'єктна система, у якої немає цієї проблеми, є в Common Lisp. Можливо, не випадково в програмах Lisp використовується багатократне успадкування.

Погано реалізоване, незручне будь-що (наприклад, багаторазове успадкування) слід ненавидіти.


2
Ваш приклад із методом length () списку та рядкових контейнерів поганий, оскільки ці два роблять абсолютно різні речі. Мета успадкування - не зменшити повторення коду. Якщо ви хочете зменшити повторення коду, перефактуруйте загальні частини в новий клас.
BЈовић

1
@ BЈовић Вони взагалі не роблять "зовсім" різних речей.
Каз

1
Розділяючи рядок і список , можна побачити багато повторень. Використовувати успадкування для реалізації цих загальних методів було б просто неправильним.
BЈовић

1
@ BЈовић У відповіді не сказано, що рядок і список повинні бути пов'язані по спадку; зовсім навпаки. Поведінка можливості замінити список або рядок в lengthоперації корисна незалежно від концепції успадкування (що в цьому випадку не допомагає: ми, ймовірно, не зможемо чогось досягти, намагаючись поділити реалізацію між два методи lengthфункції). Тим не менш, може бути деяке абстрактне успадкування: наприклад, і список, і рядок є типовою послідовністю (але це не забезпечує жодної реалізації).
Каз

2
Також успадкування - це спосіб відновлення деталей до загального класу: а саме базового класу.
Каз

1

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

(Мій приклад, можливо, не найкращий, але спробуйте отримати суть про багато простору пам’яті з тією ж метою, це перше, що мені прийшло в голову: P)

Розглянемо DDD, коли собака класу поширюється від канінуса та домашнього улюбленця; у канінус є змінна, яка вказує кількість їжі, яку вона повинна з'їсти (ціле число) під назвою dietKg, але домашня тварина також має іншу змінну для цієї мети, як правило, під тією ж самою ім'я (якщо ви не встановите інше ім'я змінної, тоді у вас буде кодифікувати додатковий код, який був початковою проблемою, яку ви хотіли уникнути, обробляти і зберігати цілісність змінних ур.), тоді у вас буде два місця пам’яті для точного з цією ж метою, щоб уникнути цього, вам доведеться модифікувати ваш компілятор, щоб він розпізнавав це ім’я під одним і тим же простором імен і просто призначив єдиний простір пам'яті для цих даних, який, на жаль, неможливо визначити під час компіляції.

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

Повірте мені людей, які реалізують цю думку дуже важко щодо всього цього, але я радий, що ви запитали, ваша видна передумова - це та, яка змінює парадигми;), і вважати це, я не кажу, що це неможливо (але багато припущень і мусить бути реалізований багатофазний компілятор і справді складний), я просто кажу, що він ще не існує, якщо ви запустите проект для власного компілятора, здатного зробити "це" (багаторазове успадкування), будь ласка, дайте мені знати, я Буду рада приєднатися до вашої команди.


1
У який момент компілятор може припустити, що змінні однойменних змінних з різних батьківських класів безпечно поєднувати? Які гарантії у вас є, що caninus.diet і pet.diet насправді служать одній цілі? Що б ви зробили з однойменною функцією / методами?
Містер Міндор

ха-ха, я повністю з вами згоден @ Mr.Mindor, саме так я і кажу у своїй відповіді. Єдиний спосіб, який мені спадає на думку наблизитися до цього підходу, - це орієнтоване на прототип програмування, але я не кажу, що це неможливо і що саме по цій причині я сказав, що багато asumptions повинні бути зроблені під час компіляції, а також деякі додаткові «дані» / especifications повинні бути написані Teh програмістом (проте , це більш кодування, яка була вихідна задача)
Ordiel

-1

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

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

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

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

Кожен взаємозв'язок у вашому коді також ускладнює розуміння та зміну фрагментів незалежно, вдвічі, при багатократному успадкуванні.

Коли ви працюєте як частина команди, ви хочете націлити на найпростіший можливий код, який абсолютно не має зайвої логіки (саме це означає DRY, а не, що ви не повинні набирати багато просто, що вам ніколи не доведеться міняти свій код за 2 місця для вирішення проблеми!)

Існують простіші способи досягти DRY-коду, ніж множинне спадкування, тому включення його в мову може відкрити вас лише до проблем, вставлених іншими, які можуть бути не на тому самому рівні розуміння, як ви. Це навіть навіть заманливо, якщо ваша мова нездатна запропонувати вам простий / менш складний спосіб зберегти свій код ДУХОМ.


Крім того, уявіть, що консультанти навчалися - я бачив так багато мов, які не є ІМ (наприклад, .net), де консультанти з глузду з’їхали з інтерфейсу на основі DI та IoC, щоб зробити все це непроникним переплутаним безладом. ІМ не робить це гірше, ймовірно, робить це краще.
gbjbaanb

@gbjbaanb Ви маєте рацію, тому, кому не спалено, зіпсувати якусь особливість легко, адже насправді це майже вимога до вивчення функції, яку ви зіпсуєте, щоб зрозуміти, як найкраще її використовувати. Мені цікаво, тому що ви, здається, говорите, що не маючи сил МВ більше DI / IoC, яких я не бачив - я б не вважав, що код консультанта, який ви отримали з усіма хитрими інтерфейсами / DI, був би краще також мати ціле божевільне багатонаціональне дерево спадку, але це може бути моя недосвідченість з MI. Чи є у вас сторінка, яку я міг прочитати про заміну DI / IoC на MI?
Білл К

-3

Найбільший аргумент проти багаторазового успадкування полягає в тому, що деякі корисні здібності можуть бути надані, а деякі корисні аксіоми можуть міститись в рамках, який сильно обмежує його (*), але не може бути наданий та / або утримуватися без таких обмежень. Серед них:

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

  • Можливість типу успадкувати реалізацію члена від батьківського класу без похідного типу повторно реалізовувати його

  • Аксіома про те, що будь-який примірник об'єкта може бути оновлений або знищений безпосередньо до себе або будь-якого з його базових типів, і такі оновлення та пониження в будь-якій комбінації чи послідовності завжди зберігають ідентичність

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

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

Якщо хочеться дозволити узагальнене багаторазове успадкування, щось інше має поступитися місцем. Якщо X і Y успадковують від B, вони обидва переміняють один і той же член M і ланцюг до базової реалізації, і якщо D успадковує від X і Y, але не перевищує M, то дається екземпляр q типу D, що слід (( В) q) .M () робити? Якщо заборонити такий склад, це порушить аксіому, яка говорить про те, що будь-який об'єкт може бути перетворений на будь-який базовий тип, але будь-яка можлива поведінка виклику групи та члена може порушити аксіому щодо методу ланцюга. Можна вимагати завантаження класів лише у поєднанні з конкретною версією базового класу, проти якої вони були складені, але це часто незручно. Якщо час відмови від завантаження будь-якого типу, у якому будь-який пращур може дістатися більш ніж одним маршрутом, може бути корисним але значно обмежила б корисність багаторазового успадкування. Дозволення спільних шляхів успадкування лише тоді, коли не існує конфліктів, створить ситуації, коли стара версія X була б сумісна зі старою або новою Y, а стара Y була б сумісною зі старою або новою X, але нова X і нова Y бути сумісним, навіть якщо ніщо не зробило нічого, що саме по собі може бути переломною.

Деякі мови та рамки дозволяють багаторазово успадковувати на теорії, що те, що отримано від ІМ, важливіше, ніж те, що потрібно відмовитись, щоб дозволити. Однак витрати MI є суттєвими, і для багатьох випадків інтерфейси забезпечують 90% переваг ІМ при невеликій частці витрат.


Чи хотіли б виборці вимагати коментарів? Я вважаю, що моя відповідь надає інформацію, відсутню в інших мовах, щодо семантичних переваг, які можна отримати, забороняючи багаторазове успадкування. Той факт, що дозволити багаторазове успадкування вимагає відмови від інших корисних речей, буде хорошим приводом для того, хто цінує інші речі більше, ніж багаторазове успадкування, протистояти ІМ.
supercat

2
Проблеми з ІМ легко вирішуються або взагалі уникаються, використовуючи ті самі методи, які ви використовували б при одному успадкуванні. Жодна з згаданих вами здібностей / аксіом не є перешкодою MI, за винятком другого ... і якщо ви успадковуєте два класи, які забезпечують різний спосіб поведінки за одним і тим же методом, ви все одно вирішите цю неоднозначність. Але якщо вам доведеться це робити, ви, мабуть, вже зловживаєте спадщиною.
cHao

@cHao: Якщо потрібно, щоб похідні класи повинні перекривати батьківські методи, а не ланцюжок на них (те саме правило, що і інтерфейси), можна уникнути проблем, але це досить серйозне обмеження. Якщо використовується шаблон, коли FirstDerivedFooпереосмислення Barне робить нічого, крім ланцюга на захищений невіртуальний FirstDerivedFoo_Bar, а SecondDerivedFooпереосмислює ланцюги на захищені невіртуальні SecondDerivedBar, і якщо код, який хоче отримати доступ до базового методу, використовує ті захищені невіртуальні методи, може бути дієвим підходом ...
supercat

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

У МІ немає властивої вимоги, що класи не викликають базових методів, і жодної конкретної причини не знадобиться. Здається, трохи дивним винуватий ІМ, враховуючи, що ця проблема також впливає на СІ.
cHao
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.