Чому я віддаю перевагу складу над спадщиною?


109

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

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

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

Class Shape
{
    string name;
  public:
    void getName();
    virtual void draw()=0;
}

Class Circle: public Shape
{
    void draw(/*draw circle*/);
}

57
Ні, коли люди кажуть, що віддають перевагу композиції, вони справді мають на увазі, що віддають перевагу композиції, а ніколи ніколи не використовують спадщину . Увесь ваш питання заснований на несправній передумові. Використовуйте спадщину, коли це доречно.
Білл Ящірка

2
Ви також можете поглянути на programmers.stackexchange.com/questions/65179/…
vjones

2
Я погоджуюся з законопроектом, я вважаю, що використання спадщини є звичайною практикою в розвитку графічного інтерфейсу.
Прашант Чолачагудда

2
Ви хочете використовувати композицію, оскільки квадрат - це лише композиція з двох трикутників, насправді я думаю, що всі форми, окрім еліпса, - це лише композиції з трикутників. Поліморфізм стосується договірних зобов’язань і на 100% віддаляється від спадщини. Коли трикутник get додає до нього купу дивацтв, тому що хтось хотів зробити його здатним генерувати піраміди, якщо ви успадковуєте від Трикутника, ви все отримаєте, хоча ви ніколи не збираєтеся створювати 3d-піраміду зі свого шестикутника .
Джиммі Хоффа

2
@BilltheLizard Я думаю, що багато людей, які говорять, це насправді означають, що ніколи не використовувати спадщину, але вони помиляються.
іммібіс

Відповіді:


51

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

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

Насправді ви в основному відповіли на власне запитання, коли сказали "But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation.". Загальна поведінка може бути забезпечена через інтерфейси та поведінковий композит.

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


На практиці, як часто проявляється "Поліморфізм через інтерфейс", і чи вважається це нормальним (на відміну від експлуатації виразності язиків). Я б сказав, що поліморфізм через успадкування був ретельним дизайном, а не деяким наслідком мови (C ++), виявленого після його конкретизації.
Саміс

1
Хто б назвав переміщення () на планеті та автомобілі і вважав би це однаковим ?! Питання тут полягає в тому, в якому контексті вони повинні мати можливість рухатися? Якщо вони обоє є двовимірними об'єктами в простій 2D грі, вони могли б успадкувати хід, якщо вони були об'єктами в масовому моделюванні даних, це може не мати особливого сенсу дозволяти їм успадковувати з однієї бази, і як наслідок, ви повинні використовувати інтерфейс
NikkyD

2
@SamusArin Він показує вгору всюди , і вважається абсолютно нормальним в мовах , які підтримують інтерфейси. Що ви маєте на увазі "експлуатація виразності мови"? Саме для цього існують інтерфейси .
Андрес Ф.

@AndresF. Я просто натрапив на "Поліморфізм через інтерфейс" на прикладі, читаючи "Об'єктно-орієнтований аналіз та дизайн із додатками", який демонстрував схему спостерігача. Потім я зрозумів свою відповідь.
Саміс

@AndresF. Я думаю, моє бачення було трохи засліплене щодо цього через те, як я використовував поліморфізм у своєму останньому (першому) проекті. У мене було 5 типів записів, які всі походять з однієї бази. Все одно дякую за просвітлення.
Саміс

79

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

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

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

Третім видом поліморфізму є підтип поліморфізм . Тут процедура, визначена для даного типу, може також працювати над цілим сімейством "підтипів" цього типу. Під час реалізації інтерфейсу або розширення класу ви, як правило, заявляєте про свій намір створити підтип. Справжні підтипи регулюються Принципом заміщення Ліскова, що говорить про те, що якщо ви можете довести щось про всі об'єкти в супертипі, ви можете довести це про всі екземпляри підтипу. Життя стає небезпечним, оскільки в таких мовах, як C ++ та Java, люди, як правило, мають невиконані, і часто недокументовані припущення про класи, які можуть бути, а можуть і не бути правдивими щодо їхніх підкласів. Тобто код пишеться так, ніби більше доказується, ніж є насправді, що створює безліч питань, коли ви недбало підтипуєте.

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

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

interface UsefulThingsInterface {
    void doThings();
    void doMoreThings();
}

...

class WayOfDoingUsefulThings implements UsefulThingsInterface{
     private foo stuff;
     public final int getStuff();
     void doThings(){
       //modifies stuff, such that ...
       ...
     }
     ...
     void doMoreThings(){
       //ignores stuff
       ...
     }
 }

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

class MyUsefulThings extends WayOfDoingUsefulThings{
     void doThings {
        //my way
     }
}

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

 void dealWithStuff(WayOfDoingUsefulThings bar){
     bar.doThings()
     use(bar.getStuff());
 }

тепер робить щось інше, ніж очікувалося, коли ви передасте це MyUsefulThings. Що гірше, ви можете навіть не знати, що WayOfDoingUsefulThingsдавали ці обіцянки. Можливо, dealWithStuffпоходить із тієї самої бібліотеки, що WayOfDoingUsefulThingsі getStuff()навіть не експортується бібліотекою (придумайте класи друзів на C ++). Що ще гірше, ви перемогли статичну перевірку мови, не усвідомлюючи цього: просто dealWithStuffвзялися за те, WayOfDoingUsefulThingsщоб переконатися, що вона буде getStuff()функціонувати певним чином.

Використання композиції

class MyUsefulThings implements UsefulThingsInterface{
     private way = new WayOfDoingUsefulThings()
     void doThings() {
        //my way
     }
     void doMoreThings() {
        this.way.doMoreThings();
     }
}

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

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

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

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

  1. Заборонити спадщину чи дизайн для неї
  2. Віддайте перевагу композиції

є гарним посібником з вищезазначених причин і не несе великих істотних витрат.


4
Гарна відповідь. Я підсумую, що намагатися досягти повторного використання коду з успадкуванням - це неправильний шлях. Спадкування дуже сильне обмеження (імхо додавання "влади" - це неправильна аналогія!) І створює сильну залежність між успадкованими класами. Занадто багато залежностей = поганий код :) Тож успадкування (він же "поводиться як") зазвичай сяє для уніфікованих інтерфейсів (= приховування складності успадкованих класів), а що-небудь ще подумайте двічі або використовуйте склад ...
MaR

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

27

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

Типовий приклад походить із світу розвитку ігор. Припустимо, у вас є базовий клас для всіх ваших ігрових об'єктів - космічного корабля гравця, монстрів, куль тощо; кожен тип сутності має свій підклас. Підхід успадкування буде використовувати кілька поліморфних методів, наприклад update_controls(), update_physics(), draw()і т.д., і реалізувати їх для кожного підкласу. Однак це означає, що ви з'єднуєте непов'язану функціональність: не важливо, як виглядає об’єкт для його переміщення, і вам не потрібно нічого знати про його AI, щоб намалювати його. Композиційний підхід натомість визначає кілька базових класів (або інтерфейсів), наприклад EntityBrain(підкласи реалізують AI або введення гравця), EntityPhysics(підкласи реалізують фізику руху) і EntityPainter(підкласи дбають про малювання) і неполіморфний класEntityщо містить один примірник кожного. Таким чином, ви можете поєднати будь-яку зовнішність з будь-якою фізичною моделлю та будь-яким ІІ, а оскільки ви будете тримати їх окремо, ваш код теж буде набагато чистішим. Також проблеми типу "Я хочу, щоб монстр, схожий на монстро з повітряної кулі на рівні 1, але поводився як божевільний клоун на рівні 15", зникає: ти просто береш відповідні компоненти і склеюєш їх разом.

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

"Розділення проблем" є ключовою фразою тут: представляти фізику, реалізувати інтелектуальний інтелект та малювати сутність - це три проблеми, об'єднання їх у сутність - четверте. При композиційному підході кожен концерн моделюється як один клас.


1
Це все добре, про що йдеться у статті, пов'язаній з оригінальним запитанням. Я б хотів (коли б у вас з’явився час), якби ви могли навести простий приклад із залученням усіх цих утворень, зберігаючи при цьому поліморфізм (у С ++). Або вкажіть мене на ресурс. Дякую.
MustafaM

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

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

1
@NikkyD Ви берете MonsterVisuals balloonVisualsта MonsterBehaviour crazyClownBehaviourінстанціюєте Monster(balloonVisuals, crazyClownBehaviour), поряд із Monster(balloonVisuals, balloonBehaviour)та Monster(crazyClownVisuals, crazyClownBehaviour)екземплярами, які були ініційовані на рівні 1 та 15 рівня
Калет

13

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

Делегування - один із прикладів способу використання композиції замість спадкування. Делегування дозволяє змінювати поведінку класу без підкласифікації. Розглянемо клас, який забезпечує підключення до мережі, NetStream. Можливо, підклас NetStream реалізує загальний мережевий протокол, тому ви можете придумати FTPStream та HTTPStream. Але замість створення дуже специфічного підкласу HTTPStream для єдиної мети, скажімо, UpdateMyWebServiceHTTPStream, часто краще використовувати звичайний старий екземпляр HTTPStream разом з делегатом, який знає, що робити з даними, які він отримує від цього об’єкта. Одна з причин, що це краще - це те, що вона уникає поширення класів, які потрібно підтримувати, але які ви ніколи не зможете повторно використовувати.


11

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

  1. Якась особливість або візерунок (назвемо це "Шаблон X") виявляється корисним для певної мети. Повідомлення в щоденниках написані на знак чеснот про Шаблон X.

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

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

  4. Люфт викликає, що деякі люди вважають, що Шаблон X завжди шкідливий і його не слід застосовувати.

Ви побачите, що цей цикл ажіотажу / зворотного зв'язку відбувається практично з будь-якою функцією, починаючи GOTOз моделей від SQL до NoSQL і, так, успадковувати. Протиотрутою є завжди враховувати контекст .

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

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

Подумайте про це як молоток проти викрутки. Чи варто віддати перевагу викрутці над молотком? Питання не має сенсу. Ви повинні використовувати інструмент, відповідний для роботи, і все залежить від того, яке завдання вам потрібно виконати.

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