Спадщина проти агрегації [закрито]


151

Існує дві школи думок про те, як найкраще розширити, вдосконалити та використати код в об'єктно-орієнтованій системі:

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

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

Які вигоди, витрати та наслідки для кожного? Чи є інші альтернативи?

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


9
Приємне запитання, на жаль, у мене зараз мало часу.
Toon Krijthe

4
Хороша відповідь - краща, ніж швидша ... Я буду дивитись власне питання, тому я проголосую за вас щонайменше :-P
Крейг Уокер

Відповіді:


181

Справа не в тому, що найкраще, а в тому, коли використовувати.

У «нормальних» випадках достатньо простого питання, щоб з’ясувати, чи потрібно нам успадкування чи об’єднання.

  • Якщо новий клас є більш-менш оригінальним класом. Використовуйте спадщину. Новий клас тепер є підкласом початкового класу.
  • Якщо новий клас повинен мати початковий клас. Використовуйте агрегацію. У новому класі тепер є початковий клас як член.

Однак є велика сіра зона. Тому нам потрібно кілька інших хитрощів.

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

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

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

Додамо приклад. У нас є клас "Собака" методами: "Їжте", "Гуляйте", "Гавкайте", "Грайте".

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Зараз нам потрібен клас «Кішка», якому потрібні «Їжте», «Прогулянка», «Мур» та «Грайте». Тому спочатку спробуйте поширити це від Собаки.

class Cat is Dog
  Purr; 
end;

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

class Cat is Dog
  Purr; 
  Bark = null;
end;

Гаразд, це працює, але пахне погано. Тож давайте спробуємо агрегацію:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

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

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

Це набагато чистіше. Жодних внутрішніх собак. І коти, і собаки на одному рівні. Ми навіть можемо представити інших домашніх тварин для розширення моделі. Якщо тільки це не риба, чи щось, що не ходить. У такому випадку нам знову потрібно рефактор. Але це щось інше.


5
"Повторне використання майже всіх функціональних можливостей класу" - це той раз, коли я фактично віддаю перевагу успадкуванню. Що мені дуже хотілося б - це мова, яка має можливість легко сказати "делегувати цьому об'єднаному об'єкту для цих конкретних методів"; це найкраще в обох світах.
Крейг Уокер

Я не впевнений в інших мовах, але в Delphi є механізм, який дозволяє членам реалізувати частину інтерфейсу.
Toon Krijthe

2
Завдання C використовує протоколи, які дозволяють вирішити, потрібна чи необов'язкова ваша функція.
cantfindaname88

1
Чудова відповідь на практичному прикладі. Я б розробив клас Pet за методом, який називається, makeSoundі дозволити кожному підкласу реалізувати свій власний звук. Тоді це допоможе в ситуаціях, коли у вас є багато домашніх тварин і просто робити це for_each pet in the list of pets, pet.makeSound.
blongho


27

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

Існують і інші розрізнення - приватне успадкування в C ++ вказує на "реалізований з точки зору" відносин, який також може бути змодельований агрегацією об'єктів-членів (не підданих впливу).


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

Мені не подобається такий підхід, це більше стосується композиції
Аліреза Рахмані Халілі

15

Ось мій найпоширеніший аргумент:

У будь-якій об'єктно-орієнтованій системі до будь-якого класу є дві частини:

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

  2. Її реалізація : робота "за кадром", яку об'єкт виконує для задоволення свого інтерфейсу та надання функціональності. Це, як правило, дані про код і член об'єкта.

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

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

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

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


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

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

Як ви вважаєте, агрегація краще в цьому сценарії? Книга - це SellingItem, а DigitalDisc - це SelliingItem codereview.stackexchange.com/questions/14077/…
LCJ,

11

Я дав відповідь на "Є" проти "Має": який з них кращий? .

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

Чи має сенс у вашому похідному класі всі методи надкласу? Або ти просто тихо обіцяєш собі, що ці методи слід ігнорувати у похідному класі? Або ви виявляєте себе переважаючими методами з надкласу, роблячи їх відсутніми, тому ніхто їх не випадково не називає? Або давати підказки вашому інструменту генерації документів API, щоб опустити метод з документа?

Це чіткі підказки, що агрегація - кращий вибір у такому випадку.


6

Я бачу багато відповідей "на - проти"; вони концептуально різні "на це та пов'язані з цим питання.

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

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

І, як я вже згадував в цій посаді, агрегація, як правило, менш гнучка, ніж спадкування.

Отже, у вас ідеальна буря аргументів проти успадкування кожного разу, коли вам доведеться вибрати той чи інший:

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

Таким чином, я схильний вибирати агрегацію - навіть коли виявляється, що є сильні відносини.


Еволюціонуючі вимоги означають, що ваша конструкція OO неминуче помиляється. Спадщина та агрегація - лише вершина цього айсберга. Ви не можете створити архітектор для всіх можливих ф’ючерсів, тому просто займіться ідеєю XP: вирішіть сьогоднішні вимоги і прийміть, що вам, можливо, доведеться робити рефактор.
Білл Карвін

Мені подобається цей напрямок допитів та розпитувань. Стурбований розмиванням is-a, has-a, і use-a, хоча. Я починаю з інтерфейсів (разом із визначенням реалізованих абстракцій, до яких я хочу інтерфейси). Додатковий рівень непрямості неоціненний. Не слідкуйте за своїм висновком про агрегацію.
orcmid

Це майже моя суть, хоча. І успадкування, і агрегація - це методи вирішення проблеми. Один із таких методів несе всілякі покарання, коли завтра доведеться вирішувати проблеми.
Крейг Уокер

На мій погляд, агрегація + інтерфейси є альтернативою успадкуванню / підкласифікації; не можна займатися поліморфізмом на основі лише агрегації.
Крейг Уокер

3

Питання зазвичай виражається як « Склад проти спадкування» , і його тут задавали раніше.


Правильно, ви .. смішні, що ТАК не дав цього як одне із суміжних питань.
Крейг Уокер

2
Тож

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

1
Так потрібна функція, щоб прокрутити 2 питання (та їх відповіді) на 1
Крейг Уокер

3

Я хотів зробити це коментарем до оригінального питання, але 300 символів кусає [; <).

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

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

Наприклад, що ви маєте на меті зробити, з чого у вас є можливість почати і які обмеження?

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

Нарешті, чи важливо зафіксувати відмінність у абстракції і як ви ставитесь до цього (як це є проти a-has-a) до різних особливостей технології OO? Можливо, так, якщо вона підтримує концептуальну структуру послідовною та керованою для вас та інших. Але розумно не бути поневоленим цим і викривленнями, які ви, можливо, в кінцевому підсумку складете. Можливо, найкраще відстояти рівень і не бути настільки жорстким (але залишити хорошу розповідь, щоб інші могли розповісти, що там). [Я шукаю те, що робить певну частину програми зрозумілою, але іноді я намагаюся вишукати, коли більший виграш. Не завжди найкраща ідея.]

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


2

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

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

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


2

Я думаю, що це не те / або дебати. Просто так:

  1. відносини is-a (успадкування) трапляються рідше, ніж стосунки a-a (композиції).
  2. Успадкування важче отримати правильно, навіть коли це доцільно використовувати, тому слід дотримуватися належної ретельності, оскільки це може порушити інкапсуляцію, заохотити жорстке сполучення, викривши реалізацію тощо.

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

Хоча, звичайно, не було б сенсу мати клас "Форма", який має класи "Точка та Квадрат". Тут спадщина обумовлена.

Люди схильні думати про спадщину спочатку, намагаючись розробити щось розширене, тобто те, що не так.


1

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

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

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


0

Обидва підходи використовуються для вирішення різних проблем. Вам не завжди потрібно об'єднувати більше двох або більше класів при успадкуванні від одного класу.

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

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