Віддаєте перевагу складу над спадщиною?


1603

Чому віддають перевагу композиції над спадщиною? Які компроміси існують для кожного підходу? Коли слід вибрати спадщину над складом?



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

1
в одному реченні успадкування є загальнодоступним, якщо у вас є публічний метод, і ви змінюєте його, воно змінює опубліковані api. якщо у вас склад і об'єкт, що склався, змінився, вам не доведеться змінювати свої опубліковані api.
Томер Бен Девід

Відповіді:


1188

Віддавайте перевагу композиції над успадковуванням, оскільки її згодом легко змінювати / легко змінювати, але не використовуйте підхід, який завжди є композицією. За складом легко змінювати поведінку під час руху за допомогою залежної інжекції / встановлення. Спадкування більш жорстке, оскільки більшість мов не дозволяють походити з більш ніж одного типу. Тож гусак більш-менш готується, коли ви походите з типу А.

Мій тест на кислоту для вищезазначеного:

  • Чи хоче TypeB розкрити повний інтерфейс (усі загальнодоступні методи не менше) TypeA, щоб TypeB можна було використовувати там, де очікується TypeA? Вказує на спадщину .

    • наприклад, біплан Cessna відкриє повний інтерфейс літака, якщо не більше. Так що це придатне для отримання літака.
  • Чи хоче TypeB лише частину / частину поведінки, викритої TypeA? Вказує на необхідність складу.

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

Оновлення: Щойно я повернувся до моєї відповіді, і зараз здається, що вона є неповною без конкретної згадки про принцип заміни Ліскова Барбари Ліськов як тест на тему "Чи слід наслідувати цей тип?"


81
Другий приклад - це прямо з книги «Перші шаблони дизайну» ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) книги :) Я настійно рекомендую цю книгу для всіх, хто з цим питанням гугла.
Єшурун

4
Це дуже зрозуміло, але може щось пропустити: "Чи хоче TypeB викрити повний інтерфейс (усі публічні методи не менше) типу TypeA, щоб TypeB можна було використовувати там, де очікується TypeA?" Але що робити, якщо це правда, і TypeB також відкриває повний інтерфейс TypeC? А що робити, якщо TypeC ще не був змодельований?
Трістан

4
Ви натякаєте на те, що, на мою думку, повинно бути найосновнішим тестом: "Чи повинен цей об’єкт використовуватись за кодом, який очікує об'єкти базового типу (що це буде)". Якщо відповідь "так", об'єкт повинен успадкувати. Якщо ні, то, мабуть, не повинно. Якби я мав свої барабани, мови надавали б ключове слово для позначення "цього класу" і забезпечувало б визначення класу, який повинен вести себе так само, як інший клас, але не бути його замінним (такий клас мав би все "це" клас "посилання замінені на себе).
supercat

22
@Alexey - справа в тому, що "Чи можу я пройти в біплані" Сессна "всім клієнтам, які очікують на літак, не дивуючи їх?". Якщо так, то, швидше за все, ви хочете отримати спадщину.
Gishu

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

414

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

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

Я не беру на себе кредиту за цей підхід. Я взяв це прямо з другого видання Code Complete, виконаного Стівом МакКоннеллом , розділ 6.3 .


107
Це не завжди ідеальний підхід, це просто хороша настанова. Принцип заміщення Ліскова набагато точніший (менший збій).
Білл К

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

36
@ Нік Звичайно, але "Мій автомобіль має поведінку автомобіля" має більше сенсу (я здогадуюсь, ваш клас "Транспортний засіб" можна назвати "Автомобільним поведінкою"). Таким чином, ви не можете базувати своє рішення на "має" проти "- це" порівняння, ви повинні використовувати LSP, або ви будете робити помилки
Tristan

35
Замість "є" думка "поводиться як". Спадщина - це спадкове поведінка, а не лише семантика.
ybakos

4
@ybakos "Поводиться як" може бути досягнуто за допомогою інтерфейсів без необхідності успадкування. З Вікіпедії : "Реалізація композиції над успадкуванням зазвичай починається зі створення різних інтерфейсів, що представляють поведінку, яку повинна проявляти система ... Таким чином, поведінка системи реалізується без успадкування".
DavidRR

210

Якщо ви розумієте різницю, це легше пояснити.

Процесуальний кодекс

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

Спадщина

Це заохочує використання занять. Спадщина - один із трьох принципів проектування ОО (спадкування, поліморфізм, інкапсуляція).

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

Це спадщина на роботі. Працівник "- це" Особа або успадковується від Особи. Усі спадкові відносини - це "є-а" відносини. Співробітник також тініє властивість Title від Person, тобто Employee.Title поверне Титул для Співробітника, а не Особи.

Склад

Склад виступає за спадщину. Простіше кажучи, вам доведеться:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

Композиція, як правило, має "стосунки" або "використовує". Тут клас Співробітник має особу. Він не успадковує від Особи, але натомість отримує переданий їй об'єкт Особи, саме тому він "має" Особу.

Склад над успадкуванням

Тепер скажіть, що ви хочете створити тип менеджера, щоб закінчити:

class Manager : Person, Employee {
   ...
}

Цей приклад спрацює чудово, однак, що якщо і особи, і працівник заявили Title? Чи повинен Manager.Title повертати "Менеджер операцій" або "Містер"? Згідно з цією неоднозначністю краще вирішувати:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

Об'єкт "Менеджер" складається як працівник та особа. Титульна поведінка береться від працівника. Цей явний склад усуває неоднозначність серед іншого, і ви будете стикатися з меншою кількістю помилок.


6
За спадщиною: Неоднозначності немає. Ви реалізуєте клас Manager на основі вимог. Таким чином, ви б повернули "Менеджер операцій", якщо це визначено вашими вимогами, інакше ви просто використаєте реалізацію базового класу. Також ви можете зробити Person абстрактним класом і тим самим переконатися, що класи низхідних потоків реалізують властивість Title.
Радж Рао

68
Важливо пам’ятати, що можна сказати «Склад над спадщиною», але це не означає «Склад завжди над спадщиною». "Є" означає успадкування і призводить до повторного використання коду. Співробітник - це особа (у працівника немає людини).
Радж Рао

36
Приклад заплутаний. Працівник - це людина, тому він повинен використовувати спадщину. Ви не повинні використовувати склад для цього прикладу, оскільки це неправильне співвідношення в доменній моделі, навіть якщо технічно ви можете оголосити це в коді.
Майкл Фрейджім

15
Я не згоден з цим прикладом. Працівник - це особа, яка є підручником у випадку правильного використання спадщини. Я також думаю, що "видавати" переглядання поля заголовка не має сенсу. Той факт, що Employee.Title тініє Person.Title - ознака поганого програмування. Зрештою, "пан" і "Менеджер з операцій" дійсно має на увазі той самий аспект людини (нижній регістр)? Я перейменував бы Employee.Title, і таким чином зможу посилатися на атрибути Title і JobTitle працівника, які мають сенс у реальному житті. Крім того, немає ніяких причин для менеджера (продовження ...)
Радон Росборо

9
(... продовження) успадковувати як від Особи, так і від працівника - адже працівник вже успадковує від Особи. У більш складних моделях, де людина може бути менеджером і агентом, правда, що багаторазове успадкування можна використовувати (обережно!), Але в багатьох середовищах було б бажано мати абстрактний клас ролей, з якого менеджер (містить співробітників s / he управляє) та агент (містить договори та іншу інформацію) успадковує. Потім працівник - це людина, яка має кілька ролей. Таким чином, і склад, і успадкування використовуються належним чином.
Радон Росборо

141

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

Недоліки спадкування:

  1. Ви не можете змінити реалізацію, успадковану від суперкласів під час виконання (очевидно, оскільки успадкування визначено під час компіляції).
  2. Успадкування розкриває підклас до деталей його виконання батьківського класу, тому часто говорять про те, що успадкування порушує інкапсуляцію (у сенсі, що вам дійсно потрібно зосередитися на інтерфейсах, а не на реалізації, тому повторне використання підкласифікації не завжди бажане).
  3. Жорстке з'єднання, що забезпечується успадкуванням, робить реалізацію підкласу дуже пов'язаною з реалізацією суперкласу, що будь-яка зміна в батьківській реалізації змусить підклас змінитися.
  4. Надмірне повторне використання підкласифікацією може зробити стек спадкування дуже глибоким і занадто заплутаним.

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


5
Це одна з кращих відповідей, на мою думку - я додам до цього, що намагаючись переосмислити ваші проблеми з точки зору складу, на мій досвід, як правило, призводить до менших, простіших, більш самостійних, більш багаторазових занять , з чіткішою, меншою, більш зосередженою сферою відповідальності. Часто це означає, що виникає менша потреба в таких речах, як ін'єкція залежності або глузування (в тестах), оскільки дрібні компоненти зазвичай здатні стояти самостійно. Просто мій досвід. YMMV :-)
mindplay.dk

3
Останній абзац у цій публікації дійсно натиснув на мене. Дякую.
Салкс

87

Інша, дуже прагматична причина - віддати перевагу композиції над успадкуванням пов'язана з вашою моделлю домену та відображенням її у реляційній базі даних. Налаштовувати спадщину на моделі SQL дуже важко (ви закінчуєте всілякі хиткі обхідні шляхи, як, наприклад, створення стовпців, які не завжди використовуються, використовуючи представлення даних тощо). Деякі ORML намагаються впоратися з цим, але це швидко ускладнюється. Склад легко моделювати за допомогою зовнішньополітичного зв’язку між двома таблицями, але успадкування набагато складніше.


81

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

Ви повинні віддавати перевагу картоплі над кока-колою, коли хочете їсти, а кока-колі над картоплею, коли хочете пити.

Створення підкласу повинно означати більше, ніж просто зручний спосіб викликати методи надкласу. Ви повинні використовувати успадкування, коли підклас "is-a" суперкласс як структурно, так і функціонально, коли він може використовуватися як надклас, і ви збираєтесь його використовувати. Якщо це не так - це не спадщина, а щось інше. Композиція - це коли ваші об'єкти складаються з іншого, або мають певне відношення до них.

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


5
Правильний інструмент для правильної роботи. Молоточок може бути краще стукати речі, ніж гайковий ключ, але це не означає, що слід розглядати гайковий ключ як "неповноцінний молоток". Спадкування може бути корисним, коли речі, додані до підкласу, необхідні, щоб об'єкт поводився як об'єкт надкласового класу. Наприклад, розглянемо базовий клас InternalCombustionEngineіз похідним класом GasolineEngine. Останній додає такі речі, як свічки запалювання, яких бракує базовому класу, але використання речі як InternalCombustionEngineволі призведе до звикання свічок запалювання.
supercat

61

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

успадкування - це, мабуть, найгірша форма зв'язку, яку ви можете мати

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

Стаття « Спадщина - це зло: Епічна невдача DataAnnotationsModelBinder» подає приклад цього в C #. Він показує використання успадкування, коли композиція повинна була використовуватися і як вона може бути відновлена.


Спадщина не є доброю чи поганою, це лише особливий випадок складу. Там, де, дійсно, підклас реалізує функціонал, аналогічний суперкласу. Якщо запропонований підклас не повторно реалізується, а лише використовує функціональність суперкласу, ви неправильно використали Спадщина. Це помилка програміста, а не роздуми про спадкування.
iPherian

42

У Java або C # об'єкт не може змінити свій тип після його інстанції.

Таким чином, якщо ваша потреба об'єкта, як інший об'єкт або поводиться по- різному в залежності від стану або умов об'єкта, а потім використовувати Склад : Зверніться до державним і стратегії шаблонами проектування.

Якщо об'єкт повинен бути одного типу, то використовуйте Спадщина або реалізуйте інтерфейси.


10
+1 Я знаходжу все менше, що спадщина працює в більшості ситуацій. Я дуже віддаю перевагу спільним / успадкованим інтерфейсам та композиціям об'єктів .... або це називається агрегацією? Не питайте мене, у мене ступінь ЕЕ !!
kenny

Я вважаю, що це найпоширеніший сценарій, коли застосовується "склад над успадкуванням", оскільки те й інше могло б відповідати теоретично. Наприклад, у системі маркетингу у вас може бути поняття "a" Client. Потім з'явиться нова концепція PreferredClient. Чи слід PreferredClientуспадковувати Client? Кращим клієнтом "є" клієнт після закінчення, ні? Ну, не так швидко ... як ви сказали, об'єкти не можуть змінити свій клас під час виконання. Як би ви моделювали client.makePreferred()операцію? Можливо, відповідь полягає у використанні композиції з відсутнім поняттям, Accountможливо?
plalx

Замість того, щоб мати різні типи Clientкласів, можливо, є лише один, який інкапсулює концепцію того, Accountщо може бути StandardAccountабо PreferredAccount...
plalx

40

Тут не знайшов задовільної відповіді, тому я написав нову.

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

Є дві переваги успадкування: підтипування та підкласифікація

  1. Підтипізація означає відповідність типу (інтерфейсу) підпису, тобто набору API, і можна замінити частину підпису для досягнення поліморфізму підтипів.

  2. Підкласифікація означає неявне повторне використання реалізацій методів.

З двома перевагами для досягнення спадкування мають дві різні цілі: орієнтовані на підтипи та орієнтацію на повторне використання коду.

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

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

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

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

  2. Спадкування забезпечує просту відкриту рекурсію через внутрішній поліморфний сайт this, тобто виклик переважаючого методу (або навіть типу ) в іншій функції-члені, державній або приватній (хоча і не рекомендується ). Відкрита рекурсія може бути змодельована за допомогою складу , але це вимагає додаткових зусиль і не завжди може бути життєздатним (?). Ця відповідь на повторне запитання говорить про щось подібне.

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

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

  5. Композиція має перевагу програмування, орієнтованого на комбінатори , тобто роботи таким чином, як композитний візерунок .

  6. Композиція негайно слідує програмуванню в інтерфейс .

  7. Склад має перевагу легкого багаторазового успадкування .

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


34

Особисто я навчився завжди віддавати перевагу композиції над спадщиною. Не існує жодної програмної проблеми, яку можна вирішити за допомогою спадщини, яку ви не можете вирішити складом; хоча вам, можливо, доведеться використовувати інтерфейси (Java) або протоколи (Obj-C) в деяких випадках. Оскільки C ++ не знає нічого подібного, вам доведеться використовувати абстрактні базові класи, а це означає, що ви не зможете повністю позбутися спадщини в C ++.

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

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

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

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


"Багато людей вважають, що спадщина представляє наш реальний світ досить добре, але це не правда". Стільки цього! На відміну від майже всіх навчальних посібників з програмування у світі, моделювання реальних об'єктів як спадкових ланцюгів, мабуть, є поганою ідеєю в перспективі. Ви повинні використовувати спадщину лише тоді, коли є неймовірно очевидні, вроджені, прості стосунки. Як і TextFileє File.
neonblitzer

25

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

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

Набагато повніша і конкретніша відповідь Тіма Будре з "Соня":

Поширені проблеми використання спадщини, як я бачу, це:

  • Невинні акти можуть мати несподівані результати . Класичний приклад цього - заклики до перезаписуваних методів з конструктора надкласового класу, перш ніж ініціалізовані поля екземплярів підкласів. У досконалому світі цього ніхто б ніколи не робив. Це не ідеальний світ.
  • Він пропонує перекручені спокуси субклассерам робити припущення про порядок викликів методів і таке - такі припущення, як правило, не є стабільними, якщо надклас може розвиватися з часом. Дивіться також аналогію мого тостеру та кавового посуду .
  • Класи стають важчими - ви не обов’язково знаєте, яку роботу виконує ваш суперклас у своєму конструкторі, або скільки пам’яті він буде використовувати. Тож побудова якогось невинного потенційного легкого об’єкта може бути набагато дорожчим, ніж ви думаєте, і це може змінитися з часом, якщо розвиватиметься суперклас
  • Це заохочує вибух підкласів . Завантаження класів коштує часу, більше класів коштує пам'яті. Це може бути проблемою, поки ви не маєте справу з додатком у масштабі NetBeans, але там у нас виникли реальні проблеми, наприклад, якщо меню є повільним, оскільки перше відображення меню викликало масове завантаження класу. Ми виправили це, перейшовши на декларативний синтаксис та інші методи, але це також коштувало часу на виправлення.
  • Згодом важче змінити речі пізніше - якщо ви зробили загальнодоступний клас, замінивши суперклас, це буде розбивати підкласи - це вибір, який після оприлюднення цього коду стане одруженим. Отже, якщо ви не змінюєте справжню функціональність у своєму суперкласі, ви отримаєте набагато більше свободи змінити речі пізніше, якщо використовуватимете, а не розширювати потрібну річ. Візьмемо, наприклад, підкласифікацію JPanel - це зазвичай неправильно; і якщо підклас десь є загальнодоступним, ви ніколи не отримаєте шансу переглянути це рішення. Якщо до нього звертається як JComponent getThePanel (), ви все одно можете це зробити (підказка: розкрийте моделі для компонентів всередині свого API).
  • Об'єктні ієрархії не масштабуються (або зробити їх масштабуванням пізніше набагато складніше, ніж планувати заздалегідь) - це класична проблема "занадто багато шарів". Я зупинюсь на цьому нижче, і як модель AskTheOracle може її вирішити (хоча це може образити пуристів OOP).

...

Я вважаю, що робити, якщо ви дозволите спадщину, яку ви можете взяти із зерном солі:

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

...

все це стосується менше малих проектів, ніж великих, і менше до приватних класів, ніж державних


25

Чому віддають перевагу композиції над спадщиною?

Дивіться інші відповіді.

Коли можна використовувати спадщину?

Часто кажуть, що клас Barможе успадковувати клас, Fooколи відповідає наступне речення:

  1. бар - foo

На жаль, вищевказаний тест не є надійним. Замість цього використовуйте наступне:

  1. бар - фу, І
  2. бари можуть робити все, що може зробити foos.

Перші випробування гарантують , що всі добувачі з Fooмає сенсу в Bar(= загальні властивості), в той час як другий тест гарантує , що всі сетера з Fooмає сенсу в Bar(= загальної функціональності).

Приклад 1: Собака -> Тварина

Собака - тварина ТА собаки можуть робити все, що можуть робити тварини (наприклад, дихати, вмирати тощо). Тому клас Dog може успадкувати клас Animal.

Приклад 2: Коло - / -> Еліпс

Коло - це еліпс, АЛЕ кола не можуть зробити все, що можуть зробити еліпси. Наприклад, кола не можуть розтягуватися, тоді як еліпси можуть. Тому клас Circle не може успадкувати клас Ellipse.

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

Коли слід використовувати спадщину?

Навіть якщо ви можете використовувати спадщину, це не означає, що слід : використання композиції - це завжди варіант. Спадкування - це потужний інструмент, що дозволяє неявне повторне використання коду та динамічну розсилку, але він має ряд недоліків, саме тому композицію часто надають перевагу. Компроміси між спадщиною та складом не очевидні, і, на мою думку, найкраще пояснити у відповіді lcn .

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

Принцип заміщення Ліскова

Так, як ви знаєте, інший метод, який використовується для визначення можливості успадкування, називається Принципом заміни Ліскова :

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

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


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

@FuzzyLogic Погодився, але насправді мій пост ніколи не виступає за використання композиції в цьому випадку. Я лише сказав, що проблема кола-еліпса є чудовим прикладом того, чому "є-а" не є хорошим тестом, щоб зробити висновок про те, що Коло має випливати з Еліпса. Як тільки ми робимо висновок, що насправді Circle не повинен випливати з Ellipse через порушення LSP, то можливими варіантами є перевернути взаємозв’язок, або використовувати композицію, або використовувати шаблонні класи, або використовувати більш складний дизайн, що включає додаткові класи або допоміжні функції, тощо ... і рішення, очевидно, повинно прийматися в кожному конкретному випадку.
Борис Дальштейн

1
@FuzzyLogic І якщо вам цікаво, що я б виступав за конкретний випадок Circle-Ellipse: я б виступав за те, щоб не реалізувати клас Circle. Проблема з інвертуванням відносин полягає в тому, що він також порушує LSP: уявіть функцію computeArea(Circle* c) { return pi * square(c->radius()); }. Він, очевидно, порушений, якщо пройшов Еліпс (що означає радіус ()?). Еліпс - це не Коло, і як таке не повинно походити від Кола.
Борис Дальштейн

computeArea(Circle *c) { return pi * width * height / 4.0; }Тепер це загальне.
Нечітка логіка

2
@FuzzyLogic Я не згоден: ви розумієте, що це означає, що клас Circle передбачив існування похідного класу Ellipse, а тому надав width()і height()? Що робити, якщо зараз користувач бібліотеки вирішить створити інший клас під назвою "EggShape"? Чи має це також походити з "Кола"? Звичайно, ні. Яйцеподібна форма - це не коло, а еліпс також не коло, тому жоден не повинен походити з кола, оскільки він порушує LSP. Методи, що виконують операції в класі Circle *, роблять важкі припущення щодо того, що таке коло, і порушення цих припущень майже напевно призведе до помилок.
Борис Дальштейн

19

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


15

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

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

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

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


3
Варто зазначити, що успадкування - це не єдиний спосіб досягти поліморфізму. Декоративний малюнок забезпечує появу поліморфізму через композицію.
BitMask777

1
@ BitMask777: Поліморфізм підтипу - це лише один вид поліморфізму, інший - параметричний поліморфізм, для цього вам не потрібно успадкування. Також важливіше: коли йдеться про спадщину, мається на увазі класичне успадкування; Якщо ви можете мати підтип поліморфізму, маючи спільний інтерфейс для декількох класів, і у вас не виникають проблеми успадкування.
egaga

2
@engaga: Я розтлумачив ваш коментар Inheritance is sometimes useful... That way you can have polymorphismяк жорстке поєднання понять успадкування та поліморфізму (підтипування передбачається з урахуванням контексту). Мій коментар мав на меті вказати на те, що ви уточнюєте у своєму коментарі: що спадкування не є єдиним способом реалізації поліморфізму, а насправді не обов'язково є визначальним фактором при вирішенні між складом та спадщиною.
BitMask777

15

Припустимо, літак має лише дві частини: двигун і крила.
Тоді є два способи проектування класу літаків.

Class Aircraft extends Engine{
  var wings;
}

Тепер ваш літак може почати з фіксованих крил
і змінити їх на обертові крила на льоту. По суті
це двигун з крилами. Але що робити, якщо я хотів також змінити
двигун на ходу?

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

Class Aircraft {
  var wings;
  var engine;
}

Тепер я можу замінити свій двигун і на льоту.


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

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


7

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

Один із прикладів цього: Ви хочете створити стек зі списку. Стек має лише поп, push і заглянути. Ви не повинні використовувати успадкування, враховуючи, що ви не бажаєте функцій push_back, push_front, removeAt та ін. У стеці.


7

Ці два способи можуть жити разом просто чудово і фактично підтримувати один одного.

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

Однак, якщо батьківський клас з якихось причин потребує доступу до функцій, наданих "дочірнім класом" для недосвідченого програміста, може здатися, що це прекрасне місце для використання успадкування. Батьківський клас може просто називати його власним абстрактним "foo ()", який перезаписується підкласом, і тоді він може надати значення абстрактній базі.

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

Чому?

Тому що успадкування - це поганий спосіб переміщення інформації .

Склад має тут реальну перевагу: відносини можуть бути перетворені: "батьківський клас" або "абстрактний працівник" можуть агрегувати будь-які конкретні "дочірні" об'єкти, що реалізують певний інтерфейс + будь-яка дитина може бути встановлена ​​всередині будь-якого іншого типу батьків, який приймає це тип . І може бути будь-яка кількість об'єктів, наприклад MergeSort або QuickSort можуть сортувати будь-який список об'єктів, що реалізують абстрактний інтерфейс Порівняння. Або кажучи іншим способом: будь-яка група об'єктів, що реалізують "foo ()" та інші групи об'єктів, які можуть використовувати об'єкти, що мають "foo ()", можуть грати разом.

Я можу придумати три реальні причини використання спадщини:

  1. У вас багато класів з одним інтерфейсом, і ви хочете заощадити час на їх написання
  2. Ви повинні використовувати один базовий клас для кожного об'єкта
  3. Вам потрібно змінити приватні змінні, які ні в якому разі не можуть бути загальнодоступними

Якщо це правда, то, мабуть, слід використовувати спадщину.

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

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

Однак якщо ви хочете використовувати приватні змінні, випадок 3, то, можливо, у вас виникнуть проблеми. Якщо ви вважаєте глобальні змінні небезпечними, то вам слід розглянути можливість використання успадкування для отримання доступу до приватних змінних також небезпечним . Зауважте, глобальні змінні - це не все, ЩО погано - бази даних є по суті великим набором глобальних змінних. Але якщо ти впораєшся, то це зовсім чудово.


7

Щоб вирішити це питання з іншого погляду для нових програмістів:

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

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

Це звучить чудово, але на практиці це майже ніколи, ніколи не працює, з однієї з декількох причин:

  • Ми виявляємо, що є деякі інші функції, які ми хочемо мати у наших класах. Якщо спосіб додавання функціональності класам здійснюється через успадкування, ми маємо вирішити - чи додаємо ми його до існуючого базового класу, хоча цей функціонал потребує не кожен клас, який успадковує його? Чи створюємо ми ще один базовий клас? А як щодо класів, які вже успадковуються від іншого базового класу?
  • Ми виявляємо, що лише для одного з класів, який успадковується від нашого базового класу, ми хочемо, щоб базовий клас поводився трохи інакше. Отже, тепер ми повертаємось і балуємося з нашим базовим класом, можливо, додаємо деякі віртуальні методи, або ще гірше, якийсь код, який говорить: "Якщо я отримав у спадок тип А, зробіть це, але якщо я успадкував тип B, зробіть це" . " Це погано з багатьох причин. Одне полягає в тому, що кожного разу, коли ми змінюємо базовий клас, ми ефективно змінюємо кожен успадкований клас. Таким чином, ми дійсно змінюємо клас A, B, C і D, оскільки нам потрібна дещо інша поведінка в класі А. Як би обережніше ми думаємо, ми можемо зламати один із цих класів з причин, які не мають нічого спільного з ними заняття.
  • Ми можемо знати, чому ми вирішили зробити так, щоб усі ці класи успадковували один одного, але це, можливо, не має сенсу для когось іншого, хто повинен підтримувати наш код. Ми можемо змусити їх зробити непростий вибір - чи роблю я щось по-справжньому потворне і безладно, щоб внести потрібні зміни (див. Попередню точку кулі) чи просто перепишу кучу цього.

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

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

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

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


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

З часу написання цієї відповіді я прочитав цей пост у "Дядька Боба", де йдеться про недостатність можливостей. Я ніколи не використовував мову, яка дозволяє багаторазово успадкувати. Але, озираючись назад, питання позначене "мовою агностик", і моя відповідь передбачає C #. Мені потрібно розширити свій кругозір.
Скотт Ханнен

6

Окрім того, що це / має свої міркування, потрібно також враховувати "глибину" спадкування, через яку повинен пройти ваш об'єкт. Все, що перевищує п’ять-шість рівнів успадкування, може спричинити несподівані проблеми з литтям, боксуванням та розпакуванням, і в цих випадках може бути розумно скласти ваш об'єкт замість цього.


6

Коли у вас є є- зв'язок між двома класами (наприклад , собака собачий), ви йдете у спадок.

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


Ви сказали спадщину та спадщину. ви не маєте на увазі спадщину та склад?
trevorKirkby

Ні, ви цього не робите. Ви можете так само добре визначити інтерфейс собак, і дозволити кожній собаці його реалізувати, і ви отримаєте більше коду SOLID.
Маркус

5

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

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


+1 для згадування інтерфейсу. Я часто використовую цей підхід, щоб приховати існуючі класи та зробити свій новий клас належним чином перевіряється, глузуючи з об’єкта, що використовується для композиції. Для цього потрібно, щоб власник нового об’єкта передав йому замість цього батьківський клас-кандидат.
Келл


4

Я погоджуюся з @Pavel, коли він каже, є місця для композиції і є місця для спадкування.

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

  • Чи є ваш клас частиною структури, яка виграє від поліморфізму? Наприклад, якщо у вас був клас Shape, який оголошує метод, який називається draw (), то нам очевидно потрібні класи Circle and Square, щоб вони були підкласами Shape, щоб їхні клієнтські класи залежали від Shape, а не від конкретних підкласів.
  • Чи потрібно вашому класу повторно використовувати будь-які взаємодії високого рівня, визначені в іншому класі? Метод шаблону шаблон дизайну буде неможливо реалізувати без успадкування. Я вважаю, що всі розширювані рамки використовують цю схему.

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


4

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

Підклас - це не те саме, що підтип. Ви можете створити підкласи, які не є підтипами (і саме тоді слід використовувати композицію). Щоб зрозуміти, що таке підтип, давайте почнемо пояснювати, що таке тип.

Коли ми кажемо, що число 5 має ціле число, ми констатуємо, що 5 належить до набору можливих значень (як приклад, див. Можливі значення для примітивних типів Java). Ми також констатуємо, що існує дійсний набір методів, які я можу виконати за значенням, таким як додавання і віднімання. І нарешті ми констатуємо, що існує набір властивостей, які завжди задовольняються, наприклад, якщо я додаю значення 3 і 5, я отримаю 8 як результат.

Щоб навести інший приклад, подумайте про абстрактні типи даних, набір цілих чисел та список цілих чисел, значення яких вони можуть містити, обмежені цілими числами. Вони обидва підтримують набір методів, як add (newValue) та size (). І вони обидва мають різні властивості (клас інваріант), Sets не допускає дублікатів, тоді як List дозволяє дублювати (звичайно, є інші властивості, якими вони задовольняють).

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

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

Потім я записую набір цілих чисел як підклас Списку цілих чисел:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

Наш набір цілих чисел - це підклас Список цілих чисел, але не є підтипом, через що він не задовольняє всім можливостям класу List. Значення та підпис методів задовольняються, але властивості не відповідають. Поведінка методу add (Integer) була чітко змінена, не зберігаючи властивостей батьківського типу. Подумайте з точки зору клієнта своїх занять. Вони можуть отримати набір цілих чисел, де очікується Список цілих чисел. Клієнт може захотіти додати значення та отримати цю вартість до Списку, навіть якщо це значення вже існує у Списку. Але її не буде мати така поведінка, якщо цінність існує. Великий сюрприз для неї!

Це класичний приклад неправильного використання спадщини. Використовуйте композицію в цьому випадку.

(фрагмент із: правильно використовувати спадщину ).


3

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


2

Композиція v / s Спадщина - це широка тема. Немає реальної відповіді на те, що краще, як на мою думку, все залежить від дизайну системи.

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

Якщо тип відношення - це "IS-A", тоді кращим є підхід. інакше тип відношення є "HAS-A", тоді склад краще підійде.

Це повністю залежить від відносин сутності.


2

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

Плюси успадкування:

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

    тобто

    Автомобіль - транспортний засіб

    Вантажівка - транспортний засіб

  2. За допомогою успадкування ви можете визначити / змінити / розширити можливості

    1. Базовий клас не забезпечує реалізації, і підклас повинен перекрити повний метод (реферат) => Ви можете реалізувати контракт
    2. Базовий клас забезпечує реалізацію за замовчуванням, а підклас може змінити поведінку => Ви можете заново визначити контракт
    3. Підклас додає розширення до реалізації базового класу, викликаючи super.methodName () як перше твердження => Ви можете продовжити контракт
    4. Базовий клас визначає структуру алгоритму, а підклас замінить частину алгоритму => Ви можете реалізувати Template_method без зміни скелета базового класу

Мінуси складу:

  1. При спадкуванні підклас може безпосередньо викликати метод базового класу , навіть якщо він не реалізує метод базового класу з IS A відносин. Якщо ви використовуєте склад, вам потрібно додати методи в контейнерному класі, щоб викрити API, що міститься

Наприклад, якщо автомобіль містить транспортний засіб, і якщо вам доведеться отримати ціну автомобіля , яка визначена в транспортному засобі , ваш код буде таким

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

Я думаю, це не відповідає на питання
almanegra

Ви можете переглянути питання щодо ОП. Я звертався до наступних питань: Які компроміси існують для кожного підходу?
Равіндра бабу

Як ви вже згадували, ви говорите лише про "плюси спадкування та мінуси складу", а не про компроміси для підходу EACH чи випадки, коли вам слід скористатися один над одним
almanegra

плюси і мінуси передбачають компроміс, оскільки плюси успадкування є мінусами складу, а мінуси складу - плюсами спадкування.
Равіндра бабу

1

Як багато хто сказав, я спершу розпочну з перевірки - чи існують стосунки "є-а". Якщо він існує, я зазвичай перевіряю наступне:

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

Наприклад 1. Бухгалтер - це працівник. Але я не буду використовувати спадщину, оскільки об'єкт Співробітник може бути екземпляром.

Напр. 2. Книга - це продаючий предмет. SellingItem неможливо встановити - це абстрактне поняття. Отже, я буду використовувати deditacne. SellingItem - це абстрактний базовий клас (або інтерфейс у C #)

Що ви думаєте про такий підхід?

Крім того, я підтримую відповідь @anon у " Чому взагалі використовувати спадщину?

Основна причина використання спадщини полягає не в формі композиції - саме так можна отримати поліморфну ​​поведінку. Якщо вам не потрібен поліморфізм, ви, ймовірно, не повинні використовувати спадщину.

@MatthieuM. йдеться в /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448

Проблема спадкування полягає в тому, що вона може використовуватися для двох ортогональних цілей:

інтерфейс (для поліморфізму)

реалізація (для повторного використання коду)

ДОВІДКА

  1. Який дизайн класу кращий?
  2. Спадщина проти агрегації

1
Я не впевнений, чому "базовий клас абстрактний?" цифри в дискусії .. LSP: чи працюватимуть усі функції, які діють на собак, якщо передані об’єкти Пуделя? Якщо так, то Пудель може бути замінений на Собаку і, отже, може успадкувати від Собаки.
Gishu

@Gishu Дякую Я обов'язково загляну в LSP. Але перед цим, чи можете ви надати "приклад того, де наслідування належне, коли базовий клас не може бути абстрактним". На мою думку, успадкування застосовується лише в тому випадку, якщо базовий клас абстрактний. Якщо базовий клас потрібно інстанціювати окремо, не переходьте до спадкування. Тобто, хоча бухгалтер є працівником, не використовуйте спадщину.
LCJ

1
останнім часом читав WCF. Прикладом у .net є SynchronizationContext (база + може бути екземпляром), які черги працюють на потоці ThreadPool. Виведення включають WinFormsSyncContext (черга на нитку інтерфейсу користувача) та DispatcherSyncContext (черга на диспетчер WPF)
Gishu

@Gishu Дякую Однак було б корисніше, якщо ви можете надати сценарій на основі домену банку, домену HR, роздрібної торгівлі або будь-якого іншого популярного домену.
LCJ

1
Вибачте. Я не знайомий з цими доменами. Іншим прикладом, якщо попередній був занадто тупим, є клас управління в Winforms / WPF. Базовий / загальний контроль може бути примірним. Похідні включають Listboxes, Textboxes і т. Д. Тепер, коли я думаю про це, шаблон дизайну Decorator є хорошим прикладом IMHO і корисним також. Декоратор виходить із необеспеченого предмета, який він хоче обернути / прикрасити.
Gishu

1

Я не бачу, щоб ніхто не згадував про проблему з алмазами , яка може виникнути при спадкуванні.

З першого погляду, якщо класи B і C успадковують A і обидва способи переосмислення методу X, а четвертий клас D успадковують і B, і C, і не змінюють X, яку реалізацію XD слід використовувати?

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


1
D успадковує B і C не A. Якщо так, то він би використовував реалізацію X, що знаходиться в класі A.
fabricio

1
@fabricio: спасибі, я редагував текст. До речі, такий сценарій не може зустрічатися в мовах, які не дозволяють успадковувати кілька класів, я прав?
Веверке

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