Чому віддають перевагу композиції над спадщиною? Які компроміси існують для кожного підходу? Коли слід вибрати спадщину над складом?
Чому віддають перевагу композиції над спадщиною? Які компроміси існують для кожного підходу? Коли слід вибрати спадщину над складом?
Відповіді:
Віддавайте перевагу композиції над успадковуванням, оскільки її згодом легко змінювати / легко змінювати, але не використовуйте підхід, який завжди є композицією. За складом легко змінювати поведінку під час руху за допомогою залежної інжекції / встановлення. Спадкування більш жорстке, оскільки більшість мов не дозволяють походити з більш ніж одного типу. Тож гусак більш-менш готується, коли ви походите з типу А.
Мій тест на кислоту для вищезазначеного:
Чи хоче TypeB розкрити повний інтерфейс (усі загальнодоступні методи не менше) TypeA, щоб TypeB можна було використовувати там, де очікується TypeA? Вказує на спадщину .
Чи хоче TypeB лише частину / частину поведінки, викритої TypeA? Вказує на необхідність складу.
Оновлення: Щойно я повернувся до моєї відповіді, і зараз здається, що вона є неповною без конкретної згадки про принцип заміни Ліскова Барбари Ліськов як тест на тему "Чи слід наслідувати цей тип?"
Подумайте про стримування як про відносини. Автомобіль "має" двигун, людина "має" ім'я тощо.
Подумайте про спадкування як це взаємини. Автомобіль "- це" транспортний засіб, людина "-" ссавець тощо.
Я не беру на себе кредиту за цей підхід. Я взяв це прямо з другого видання Code Complete, виконаного Стівом МакКоннеллом , розділ 6.3 .
Якщо ви розумієте різницю, це легше пояснити.
Прикладом цього є 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;
}
}
Об'єкт "Менеджер" складається як працівник та особа. Титульна поведінка береться від працівника. Цей явний склад усуває неоднозначність серед іншого, і ви будете стикатися з меншою кількістю помилок.
З усіма незаперечними перевагами, отриманими у спадщину, ось деякі його недоліки.
Недоліки спадкування:
З іншого боку, композиція об'єкта визначається під час виконання через об'єкти, що набувають посилань на інші об'єкти. У такому випадку ці об'єкти ніколи не зможуть дістатися захищених даних один одного (не відбувається перерви інкапсуляції) і будуть змушені поважати інтерфейс один одного. І в цьому випадку залежностей від реалізації буде набагато менше, ніж у випадку успадкування.
Інша, дуже прагматична причина - віддати перевагу композиції над успадкуванням пов'язана з вашою моделлю домену та відображенням її у реляційній базі даних. Налаштовувати спадщину на моделі SQL дуже важко (ви закінчуєте всілякі хиткі обхідні шляхи, як, наприклад, створення стовпців, які не завжди використовуються, використовуючи представлення даних тощо). Деякі ORML намагаються впоратися з цим, але це швидко ускладнюється. Склад легко моделювати за допомогою зовнішньополітичного зв’язку між двома таблицями, але успадкування набагато складніше.
Хоча в коротких словах я б погодився з "Віддавайте перевагу складу над спадщиною", дуже часто для мене це звучить як "віддайте перевагу картоплі над кока-колою". Є місця для спадкування та місця для композиції. Вам потрібно зрозуміти різницю, тоді це питання зникне. Що насправді означає для мене це "якщо ти збираєшся використовувати спадщину - подумай ще раз, швидше за все, потрібен склад".
Ви повинні віддавати перевагу картоплі над кока-колою, коли хочете їсти, а кока-колі над картоплею, коли хочете пити.
Створення підкласу повинно означати більше, ніж просто зручний спосіб викликати методи надкласу. Ви повинні використовувати успадкування, коли підклас "is-a" суперкласс як структурно, так і функціонально, коли він може використовуватися як надклас, і ви збираєтесь його використовувати. Якщо це не так - це не спадщина, а щось інше. Композиція - це коли ваші об'єкти складаються з іншого, або мають певне відношення до них.
Тож для мене це виглядає так, якщо хтось не знає, чи потрібні йому спадок або склад, справжня проблема полягає в тому, що він не знає, хоче він пити чи їсти. Подумайте більше про свою проблемну область, краще зрозумійте її.
InternalCombustionEngine
із похідним класом GasolineEngine
. Останній додає такі речі, як свічки запалювання, яких бракує базовому класу, але використання речі як InternalCombustionEngine
волі призведе до звикання свічок запалювання.
Спадщина є дуже привабливою, особливо з процесуальних питань, і вона часто виглядає оманливо елегантною. Я маю на увазі, що все, що мені потрібно зробити, - це додати цей трохи функціональності до якогось іншого класу, правда? Ну, одна з проблем полягає в тому
Ваш базовий клас розбиває інкапсуляцію, виставляючи деталі реалізації підкласам у вигляді захищених членів. Це робить вашу систему жорсткою та крихкою. Більш трагічним недоліком є те, що новий підклас приносить із собою весь багаж та думку ланцюжка спадкування.
Стаття « Спадщина - це зло: Епічна невдача DataAnnotationsModelBinder» подає приклад цього в C #. Він показує використання успадкування, коли композиція повинна була використовуватися і як вона може бути відновлена.
У Java або C # об'єкт не може змінити свій тип після його інстанції.
Таким чином, якщо ваша потреба об'єкта, як інший об'єкт або поводиться по- різному в залежності від стану або умов об'єкта, а потім використовувати Склад : Зверніться до державним і стратегії шаблонами проектування.
Якщо об'єкт повинен бути одного типу, то використовуйте Спадщина або реалізуйте інтерфейси.
Client
. Потім з'явиться нова концепція PreferredClient
. Чи слід PreferredClient
успадковувати Client
? Кращим клієнтом "є" клієнт після закінчення, ні? Ну, не так швидко ... як ви сказали, об'єкти не можуть змінити свій клас під час виконання. Як би ви моделювали client.makePreferred()
операцію? Можливо, відповідь полягає у використанні композиції з відсутнім поняттям, Account
можливо?
Client
класів, можливо, є лише один, який інкапсулює концепцію того, Account
що може бути StandardAccount
або PreferredAccount
...
Тут не знайшов задовільної відповіді, тому я написав нову.
Щоб зрозуміти, чому " віддають перевагу композиції над спадщиною", нам потрібно спочатку повернути припущення, опущене в цій скороченій ідіомі.
Є дві переваги успадкування: підтипування та підкласифікація
Підтипізація означає відповідність типу (інтерфейсу) підпису, тобто набору API, і можна замінити частину підпису для досягнення поліморфізму підтипів.
Підкласифікація означає неявне повторне використання реалізацій методів.
З двома перевагами для досягнення спадкування мають дві різні цілі: орієнтовані на підтипи та орієнтацію на повторне використання коду.
Якщо повторне використання коду є єдиною метою, то підкласифікація може дати більше, ніж те, що йому потрібно, тобто деякі публічні методи батьківського класу не мають великого сенсу для дочірнього класу. У цьому випадку замість того, щоб віддавати перевагу складу над спадщиною, вимагається композиція . Тут також походить поняття "є-а" проти "має-а".
Тож лише тоді, коли призначене підтипування, тобто використання нового класу в поліморфному порядку, ми стикаємося з проблемою вибору спадщини або складу. Це припущення, яке опускається в скороченій ідіомі, що обговорюється.
Для підтипу - це відповідність підпису типу, це означає, що композиція завжди повинна виставляти не меншу кількість API цього типу. Тепер розпродажі починають:
Спадкування забезпечує просте повторне використання коду, якщо його не відміняють, тоді як композиція повинна перекодувати кожен API, навіть якщо це лише проста робота делегування.
Спадкування забезпечує просту відкриту рекурсію через внутрішній поліморфний сайт this
, тобто виклик переважаючого методу (або навіть типу ) в іншій функції-члені, державній або приватній (хоча і не рекомендується ). Відкрита рекурсія може бути змодельована за допомогою складу , але це вимагає додаткових зусиль і не завжди може бути життєздатним (?). Ця відповідь на повторне запитання говорить про щось подібне.
Спадщина виставляє захищених членів. Це порушує інкапсуляцію батьківського класу, і якщо він використовується підкласом, вводиться інша залежність між дитиною та її батьком.
Склад має перевагу інверсії управління, і його залежність можна вводити динамічно, як це показано на малюнку декоратора та проксі-схемі .
Композиція має перевагу програмування, орієнтованого на комбінатори , тобто роботи таким чином, як композитний візерунок .
Композиція негайно слідує програмуванню в інтерфейс .
Склад має перевагу легкого багаторазового успадкування .
Маючи на увазі вищезгадані компроміси, тому ми віддаємо перевагу складу над спадщиною. Але для жорстко пов'язаних класів, тобто коли неявне повторне використання коду дійсно приносить користь або бажана магічна сила відкритої рекурсії, вибір має бути спадком.
Особисто я навчився завжди віддавати перевагу композиції над спадщиною. Не існує жодної програмної проблеми, яку можна вирішити за допомогою спадщини, яку ви не можете вирішити складом; хоча вам, можливо, доведеться використовувати інтерфейси (Java) або протоколи (Obj-C) в деяких випадках. Оскільки C ++ не знає нічого подібного, вам доведеться використовувати абстрактні базові класи, а це означає, що ви не зможете повністю позбутися спадщини в C ++.
Композиція часто більш логічна, вона забезпечує кращу абстракцію, кращу інкапсуляцію, краще повторне використання коду (особливо в дуже великих проектах) і менше шансів зламати щось на відстані лише тому, що ви внесли поодинокі зміни в будь-якому місці свого коду. Це також полегшує дотримання " Принципу єдиної відповідальності ", який часто підсумовується як " Ніколи не повинно бути більше однієї причини для зміни класу ". Це означає, що кожен клас існує з певною метою, і він повинен є лише методи, безпосередньо пов'язані з його призначенням. Також наявність дуже неглибокого дерева успадкування значно полегшує збереження огляду, навіть коли ваш проект починає отримувати дійсно великі розміри. Багато людей думають, що спадщина представляє наш реальний світдосить добре, але це не правда. Реальний світ використовує набагато більше складу, ніж спадщину. Практично кожен предмет реального світу, який ви можете тримати в руці, складається з інших, менших об'єктів реального світу.
Однак є і складові. Якщо ви пропустили успадкування і зосередили увагу лише на складі, ви помітите, що вам часто доводиться писати пару додаткових рядків коду, які були б вам не потрібні, якщо ви використовували спадщину. Ви також іноді змушені повторювати себе, і це порушує Принцип DRY(DRY = Не повторюй себе). Також композиція часто вимагає делегування, а метод просто викликає інший метод іншого об'єкта без іншого коду, що оточує цей виклик. Такі "виклики подвійного методу" (які можуть легко поширюватися на потрійні або чотириразові виклики методу і навіть далі від цього) мають набагато гіршу продуктивність, ніж успадкування, коли ви просто успадковуєте метод свого батька. Виклик успадкованого методу може бути настільки ж швидким, як і виклик не успадкованого, або він може бути дещо повільнішим, але, як правило, все-таки швидше, ніж два послідовних виклику методу.
Можливо, ви помітили, що більшість мов OO не допускають багаторазового успадкування. Хоча є кілька випадків, коли багатократне успадкування дійсно може вам щось купити, але це скоріше винятки, ніж правило. Кожен раз, коли ви стикаєтеся з ситуацією, коли ви думаєте, що "багаторазове успадкування було б дійсно класною особливістю вирішити цю проблему", ви зазвичай знаходитесь в точці, коли вам слід переосмислити спадщину взагалі, оскільки навіть це може зажадати пару додаткових рядків коду , рішення на основі композиції, як правило, виявляться набагато більш елегантним, гнучким і майбутнім доказом.
Спадщина - це справді прикольна особливість, але, боюся, останніми роками це було перебільшено. Люди ставилися до спадщини як до одного молотка, який може забити все це, незалежно від того, чи це був насправді цвях, гвинт чи, можливо, щось зовсім інше.
TextFile
є File
.
Моє загальне правило: Перш ніж використовувати спадщину, подумайте, чи має склад більше сенсу.
Причина: Підкласифікація зазвичай означає більшу складність та зв’язність, тобто важче змінювати, підтримувати та масштабувати, не допускаючи помилок.
Набагато повніша і конкретніша відповідь Тіма Будре з "Соня":
Поширені проблеми використання спадщини, як я бачу, це:
- Невинні акти можуть мати несподівані результати . Класичний приклад цього - заклики до перезаписуваних методів з конструктора надкласового класу, перш ніж ініціалізовані поля екземплярів підкласів. У досконалому світі цього ніхто б ніколи не робив. Це не ідеальний світ.
- Він пропонує перекручені спокуси субклассерам робити припущення про порядок викликів методів і таке - такі припущення, як правило, не є стабільними, якщо надклас може розвиватися з часом. Дивіться також аналогію мого тостеру та кавового посуду .
- Класи стають важчими - ви не обов’язково знаєте, яку роботу виконує ваш суперклас у своєму конструкторі, або скільки пам’яті він буде використовувати. Тож побудова якогось невинного потенційного легкого об’єкта може бути набагато дорожчим, ніж ви думаєте, і це може змінитися з часом, якщо розвиватиметься суперклас
- Це заохочує вибух підкласів . Завантаження класів коштує часу, більше класів коштує пам'яті. Це може бути проблемою, поки ви не маєте справу з додатком у масштабі NetBeans, але там у нас виникли реальні проблеми, наприклад, якщо меню є повільним, оскільки перше відображення меню викликало масове завантаження класу. Ми виправили це, перейшовши на декларативний синтаксис та інші методи, але це також коштувало часу на виправлення.
- Згодом важче змінити речі пізніше - якщо ви зробили загальнодоступний клас, замінивши суперклас, це буде розбивати підкласи - це вибір, який після оприлюднення цього коду стане одруженим. Отже, якщо ви не змінюєте справжню функціональність у своєму суперкласі, ви отримаєте набагато більше свободи змінити речі пізніше, якщо використовуватимете, а не розширювати потрібну річ. Візьмемо, наприклад, підкласифікацію JPanel - це зазвичай неправильно; і якщо підклас десь є загальнодоступним, ви ніколи не отримаєте шансу переглянути це рішення. Якщо до нього звертається як JComponent getThePanel (), ви все одно можете це зробити (підказка: розкрийте моделі для компонентів всередині свого API).
- Об'єктні ієрархії не масштабуються (або зробити їх масштабуванням пізніше набагато складніше, ніж планувати заздалегідь) - це класична проблема "занадто багато шарів". Я зупинюсь на цьому нижче, і як модель AskTheOracle може її вирішити (хоча це може образити пуристів OOP).
...
Я вважаю, що робити, якщо ви дозволите спадщину, яку ви можете взяти із зерном солі:
- Не піддавайте жодних полів, крім констант
- Методи мають бути або абстрактними, або остаточними
- Не викликайте методів від конструктора суперкласу
...
все це стосується менше малих проектів, ніж великих, і менше до приватних класів, ніж державних
Дивіться інші відповіді.
Часто кажуть, що клас Bar
може успадковувати клас, Foo
коли відповідає наступне речення:
- бар - foo
На жаль, вищевказаний тест не є надійним. Замість цього використовуйте наступне:
- бар - фу, І
- бари можуть робити все, що може зробити foos.
Перші випробування гарантують , що всі добувачі з Foo
має сенсу в Bar
(= загальні властивості), в той час як другий тест гарантує , що всі сетера з Foo
має сенсу в Bar
(= загальної функціональності).
Приклад 1: Собака -> Тварина
Собака - тварина ТА собаки можуть робити все, що можуть робити тварини (наприклад, дихати, вмирати тощо). Тому клас Dog
може успадкувати клас Animal
.
Приклад 2: Коло - / -> Еліпс
Коло - це еліпс, АЛЕ кола не можуть зробити все, що можуть зробити еліпси. Наприклад, кола не можуть розтягуватися, тоді як еліпси можуть. Тому клас Circle
не може успадкувати клас Ellipse
.
Це називається проблемою « Коло-Еліпс» , яка насправді не є проблемою, лише чіткий доказ того, що перший тест лише недостатній, щоб зробити висновок про можливе успадкування. Зокрема, цей приклад підкреслює, що похідні класи повинні розширювати функціональність базових класів, а не обмежувати його. В іншому випадку базовий клас не може бути використаний поліморфно.
Навіть якщо ви можете використовувати спадщину, це не означає, що слід : використання композиції - це завжди варіант. Спадкування - це потужний інструмент, що дозволяє неявне повторне використання коду та динамічну розсилку, але він має ряд недоліків, саме тому композицію часто надають перевагу. Компроміси між спадщиною та складом не очевидні, і, на мою думку, найкраще пояснити у відповіді lcn .
Як правило, я схильний вибирати спадщину над композицією, коли очікується, що поліморфне використання буде дуже поширеним, і в цьому випадку потужність динамічної диспетчеризації може призвести до набагато більш читабельного та елегантного API. Наприклад, наявність поліморфного класу Widget
в рамках графічного інтерфейсу або поліморфного класу Node
в бібліотеках XML дозволяє мати API, який можна зрозуміти і зрозуміти інтуїтивніше, ніж те, що було б у вас із рішенням, суто заснованим на композиції.
Так, як ви знаєте, інший метод, який використовується для визначення можливості успадкування, називається Принципом заміни Ліскова :
Функції, що використовують покажчики або посилання на базові класи, повинні мати можливість використовувати об'єкти похідних класів, не знаючи цього
По суті, це означає, що успадкування можливе, якщо базовий клас можна використовувати поліморфно, що, на мою думку, еквівалентне нашому тесту "бар є foo і бари можуть робити все, що може зробити foos".
computeArea(Circle* c) { return pi * square(c->radius()); }
. Він, очевидно, порушений, якщо пройшов Еліпс (що означає радіус ()?). Еліпс - це не Коло, і як таке не повинно походити від Кола.
computeArea(Circle *c) { return pi * width * height / 4.0; }
Тепер це загальне.
width()
і height()
? Що робити, якщо зараз користувач бібліотеки вирішить створити інший клас під назвою "EggShape"? Чи має це також походити з "Кола"? Звичайно, ні. Яйцеподібна форма - це не коло, а еліпс також не коло, тому жоден не повинен походити з кола, оскільки він порушує LSP. Методи, що виконують операції в класі Circle *, роблять важкі припущення щодо того, що таке коло, і порушення цих припущень майже напевно призведе до помилок.
Спадкування дуже потужне, але ви не можете його примусити (див .: проблема кола-еліпса ). Якщо ви справді не можете бути повністю впевнені у справжніх підтипах "є-а", тоді найкраще йти з композицією.
Спадщина створює міцний зв’язок між підкласом та суперкласом; підклас повинен знати деталі реалізації суперкласу. Створити суперкласс набагато складніше, коли потрібно думати про те, як його можна продовжити. Ви повинні ретельно задокументувати інваріанти класів та вказати, які інші методи перезаписувані методи використовують внутрішньо.
Спадщина іноді корисна, якщо ієрархія справді являє собою взаємозв'язок. Він стосується принципу відкритого закриття, де зазначено, що класи повинні бути закритими для модифікації, але відкритими для розширення. Таким чином у вас може виникнути поліморфізм; мати загальний метод, який стосується супертипу та його методів, але за допомогою динамічної диспетчерики викликається метод підкласу. Це гнучко і допомагає створити опосередкованість, що важливо в програмному забезпеченні (щоб менше знати про деталі реалізації).
Успадкування легко використовуватись, проте створює додаткові складності з жорсткою залежністю між класами. Також розуміння того, що відбувається під час виконання програми, стає досить важким через шари та динамічний вибір викликів методів.
Я б запропонував використовувати композицію за замовчуванням. Він більш модульний і дає перевагу пізнього зв’язування (можна змінити компонент динамічно). Також простіше тестувати речі окремо. І якщо вам потрібно використовувати метод з класу, ви не змушені бути певної форми (Принцип заміщення Ліскова).
Inheritance is sometimes useful... That way you can have polymorphism
як жорстке поєднання понять успадкування та поліморфізму (підтипування передбачається з урахуванням контексту). Мій коментар мав на меті вказати на те, що ви уточнюєте у своєму коментарі: що спадкування не є єдиним способом реалізації поліморфізму, а насправді не обов'язково є визначальним фактором при вирішенні між складом та спадщиною.
Припустимо, літак має лише дві частини: двигун і крила.
Тоді є два способи проектування класу літаків.
Class Aircraft extends Engine{
var wings;
}
Тепер ваш літак може почати з фіксованих крил
і змінити їх на обертові крила на льоту. По суті
це двигун з крилами. Але що робити, якщо я хотів також змінити
двигун на ходу?
Або базовий клас Engine
виставляє мутатор для зміни його
властивостей, або я переконструюю Aircraft
як:
Class Aircraft {
var wings;
var engine;
}
Тепер я можу замінити свій двигун і на льоту.
Потрібно ознайомитись із принципом заміщення Ліскова в принципах дизайну класу SOLID дядька Боба . :)
Коли ви хочете "скопіювати" / розкрити API базового класу, ви використовуєте спадщину. Коли ви хочете лише "скопіювати" функціонал, використовуйте делегування.
Один із прикладів цього: Ви хочете створити стек зі списку. Стек має лише поп, push і заглянути. Ви не повинні використовувати успадкування, враховуючи, що ви не бажаєте функцій push_back, push_front, removeAt та ін. У стеці.
Ці два способи можуть жити разом просто чудово і фактично підтримувати один одного.
Композиція просто відтворює його модульно: ви створюєте інтерфейс, подібний до батьківського класу, створюєте новий об'єкт і делегуєте йому виклики. Якщо цим об’єктам не потрібно знати один одного, це цілком безпечна і проста у використанні композиція. Тут так багато можливостей.
Однак, якщо батьківський клас з якихось причин потребує доступу до функцій, наданих "дочірнім класом" для недосвідченого програміста, може здатися, що це прекрасне місце для використання успадкування. Батьківський клас може просто називати його власним абстрактним "foo ()", який перезаписується підкласом, і тоді він може надати значення абстрактній базі.
Це виглядає як приємна ідея, але в багатьох випадках краще просто дати класу об'єкт, який реалізує foo () (або навіть встановити значення, надане foo () вручну), ніж успадкувати новий клас від якогось базового класу, який вимагає функцію foo (), яку потрібно вказати.
Чому?
Тому що успадкування - це поганий спосіб переміщення інформації .
Склад має тут реальну перевагу: відносини можуть бути перетворені: "батьківський клас" або "абстрактний працівник" можуть агрегувати будь-які конкретні "дочірні" об'єкти, що реалізують певний інтерфейс + будь-яка дитина може бути встановлена всередині будь-якого іншого типу батьків, який приймає це тип . І може бути будь-яка кількість об'єктів, наприклад MergeSort або QuickSort можуть сортувати будь-який список об'єктів, що реалізують абстрактний інтерфейс Порівняння. Або кажучи іншим способом: будь-яка група об'єктів, що реалізують "foo ()" та інші групи об'єктів, які можуть використовувати об'єкти, що мають "foo ()", можуть грати разом.
Я можу придумати три реальні причини використання спадщини:
Якщо це правда, то, мабуть, слід використовувати спадщину.
Немає нічого поганого у використанні причини 1, дуже добре мати надійний інтерфейс на ваших об'єктах. Це можна зробити за допомогою складу або з успадкуванням, без проблем - якщо цей інтерфейс простий і не змінюється. Зазвичай спадкування тут досить ефективно.
Якщо причина номер 2, вона стає трохи хитрою. Вам справді потрібно використовувати лише той самий базовий клас? Взагалі, використання одного і того ж базового класу недостатньо добре, але це може бути вимогою вашої основи, розгляду конструкції якої не уникнути.
Однак якщо ви хочете використовувати приватні змінні, випадок 3, то, можливо, у вас виникнуть проблеми. Якщо ви вважаєте глобальні змінні небезпечними, то вам слід розглянути можливість використання успадкування для отримання доступу до приватних змінних також небезпечним . Зауважте, глобальні змінні - це не все, ЩО погано - бази даних є по суті великим набором глобальних змінних. Але якщо ти впораєшся, то це зовсім чудово.
Щоб вирішити це питання з іншого погляду для нових програмістів:
Успадкування часто навчається рано, коли ми вивчаємо об'єктно-орієнтоване програмування, тому це розглядається як просте рішення загальної проблеми.
У мене є три класи, які потребують загального функціоналу. Тож, якщо я напишу базовий клас і з усіма їм успадкуватимуть його, то всі вони матимуть таку функціональність, і мені потрібно буде лише підтримувати його колись.
Це звучить чудово, але на практиці це майже ніколи, ніколи не працює, з однієї з декількох причин:
Зрештою, ми зав'язуємо наш код у деяких складних вузлах і не отримуємо від цього ніякої користі, за винятком того, що ми можемо сказати: "Класно, я дізнався про спадщину, і тепер я ним скористався". Це не означало поблажливості, тому що ми це все зробили. Але ми всі це робили, тому що ніхто не казав нам цього не робити.
Як тільки хтось пояснив мені "користь композиції над спадщиною", я замислювався над кожним разом, коли намагався розділити функціональність між класами, використовуючи успадкування, і зрозумів, що більшість часу це не дуже добре працює.
Протиотрута - це єдиний принцип відповідальності . Подумайте про це як про обмеження. Мій клас повинен робити одне. Я повинен бути в змозі дати своєму класу ім’я, яке якось описує те, що він робить. (З усього є винятки, але абсолютні правила іноді краще, коли ми навчаємось.) Звідси випливає, що я не можу написати базовий клас, який називається ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
. Яка б окрема функціональність мені не потрібна, вона повинна бути у власному класі, а тоді інші класи, які потребують цієї функціональності, можуть залежати від цього класу, а не успадковувати його.
Загрожує надмірним спрощенням, ось така композиція - складання кількох класів для спільної роботи. І як тільки ми формуємо цю звичку, ми виявляємо, що вона набагато гнучкіша, ремонтованіша і перевірена, ніж використання успадкування.
Окрім того, що це / має свої міркування, потрібно також враховувати "глибину" спадкування, через яку повинен пройти ваш об'єкт. Все, що перевищує п’ять-шість рівнів успадкування, може спричинити несподівані проблеми з литтям, боксуванням та розпакуванням, і в цих випадках може бути розумно скласти ваш об'єкт замість цього.
Коли у вас є є- зв'язок між двома класами (наприклад , собака собачий), ви йдете у спадок.
З іншого боку , якщо у вас є є-а або яке - то ставлення прикметника між двома класами (студент курсів) або (дослідження вчителів курси), ви вибрали композицію.
Простий спосіб зрозуміти це в тому, що спадщину слід використовувати, коли вам потрібен об'єкт вашого класу, який має той самий інтерфейс, як і його батьківський клас, щоб він таким чином міг трактуватися як об'єкт батьківського класу (оновлення) . Крім того, виклики функцій на об'єкті похідного класу будуть залишатися такими ж всюди в коді, але методи специфічні для виклику будуть визначатися під час виконання (тобто низькорівневих реалізації відрізняються, високорівнева інтерфейс залишається незмінний).
Композицію слід використовувати, коли вам не потрібно, щоб новий клас мав той самий інтерфейс, тобто ви хочете приховати певні аспекти реалізації класу, про які користувач цього класу не повинен знати. Таким чином, композиція скоріше підтримує інкапсуляцію (тобто приховує реалізацію), тоді як успадкування має на увазі підтримку абстрагування (тобто надання спрощеного представлення чогось, в даному випадку того ж інтерфейсу для діапазону типів з різними внутрішніми).
Підтипи підходять і є більш потужними там, де інваріанти можуть бути перераховані , в іншому випадку використовуйте функціональну композицію для розширення.
Я погоджуюся з @Pavel, коли він каже, є місця для композиції і є місця для спадкування.
Я думаю, що спадщину слід використовувати, якщо ваша відповідь є позитивною на будь-яке з цих питань.
Однак, якщо ваш намір полягає виключно у використанні коду, то, швидше за все, композиція - кращий вибір дизайну.
Спадкування - це дуже потужний маханізм для використання коду. Але його потрібно правильно використовувати. Я б сказав, що успадкування використовується правильно, якщо підклас також є підтипом батьківського класу. Як було сказано вище, тут є ключовим моментом Лісовського принципу заміни.
Підклас - це не те саме, що підтип. Ви можете створити підкласи, які не є підтипами (і саме тоді слід використовувати композицію). Щоб зрозуміти, що таке підтип, давайте почнемо пояснювати, що таке тип.
Коли ми кажемо, що число 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) була чітко змінена, не зберігаючи властивостей батьківського типу. Подумайте з точки зору клієнта своїх занять. Вони можуть отримати набір цілих чисел, де очікується Список цілих чисел. Клієнт може захотіти додати значення та отримати цю вартість до Списку, навіть якщо це значення вже існує у Списку. Але її не буде мати така поведінка, якщо цінність існує. Великий сюрприз для неї!
Це класичний приклад неправильного використання спадщини. Використовуйте композицію в цьому випадку.
(фрагмент із: правильно використовувати спадщину ).
Я чув, як я чув, що спадщину слід використовувати, коли його "є-а" відносини і склад, коли його "є-є". Навіть при цьому я відчуваю, що завжди слід схилятися до композиції, оскільки це виключає велику складність.
Композиція v / s Спадщина - це широка тема. Немає реальної відповіді на те, що краще, як на мою думку, все залежить від дизайну системи.
Як правило, тип взаємозв'язку між об'єктом забезпечує кращу інформацію для вибору одного з них.
Якщо тип відношення - це "IS-A", тоді кращим є підхід. інакше тип відношення є "HAS-A", тоді склад краще підійде.
Це повністю залежить від відносин сутності.
Незважаючи на те, що композиція є кращою, я хотів би виділити плюси спадкування та мінуси складу .
Плюси успадкування:
Він встановлює логічне відношення " IS A" . Якщо автомобіль та вантажівка - це два типи транспортних засобів (базовий клас), дочірній клас - це базовий клас.
тобто
Автомобіль - транспортний засіб
Вантажівка - транспортний засіб
За допомогою успадкування ви можете визначити / змінити / розширити можливості
Мінуси складу:
Наприклад, якщо автомобіль містить транспортний засіб, і якщо вам доведеться отримати ціну автомобіля , яка визначена в транспортному засобі , ваш код буде таким
class Vehicle{
protected double getPrice(){
// return price
}
}
class Car{
Vehicle vehicle;
protected double getPrice(){
return vehicle.getPrice();
}
}
Як багато хто сказав, я спершу розпочну з перевірки - чи існують стосунки "є-а". Якщо він існує, я зазвичай перевіряю наступне:
Чи може бути базовий клас екземпляром. Тобто, чи може базовий клас бути неабразованим. Якщо це може бути не абстрактно, я зазвичай віддаю перевагу композиції
Наприклад 1. Бухгалтер - це працівник. Але я не буду використовувати спадщину, оскільки об'єкт Співробітник може бути екземпляром.
Напр. 2. Книга - це продаючий предмет. SellingItem неможливо встановити - це абстрактне поняття. Отже, я буду використовувати deditacne. SellingItem - це абстрактний базовий клас (або інтерфейс у C #)
Що ви думаєте про такий підхід?
Крім того, я підтримую відповідь @anon у " Чому взагалі використовувати спадщину?
Основна причина використання спадщини полягає не в формі композиції - саме так можна отримати поліморфну поведінку. Якщо вам не потрібен поліморфізм, ви, ймовірно, не повинні використовувати спадщину.
@MatthieuM. йдеться в /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448
Проблема спадкування полягає в тому, що вона може використовуватися для двох ортогональних цілей:
інтерфейс (для поліморфізму)
реалізація (для повторного використання коду)
ДОВІДКА
Я не бачу, щоб ніхто не згадував про проблему з алмазами , яка може виникнути при спадкуванні.
З першого погляду, якщо класи B і C успадковують A і обидва способи переосмислення методу X, а четвертий клас D успадковують і B, і C, і не змінюють X, яку реалізацію XD слід використовувати?
Вікіпедія пропонує хороший огляд теми, про яку йдеться в цьому питанні.