Чи слід уникати використання об'єктів успадкування як можливих для розробки гри?


36

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

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

Я використовую абстрактний базовий клас, що - щось на зразок WeaponBaseі створення спеціальних класів зброї , як Shotgun, AssaultRifle, Machinegunщо - щось подібне. Є так багато переваг, і один з образів, який мені дуже подобається - це поліморфізм. Я можу розцінювати об'єкт, створений підкласами, як би базовим класом, щоб логіка могла бути різко зменшена і зробити її більш багаторазовою. Якщо мені потрібно отримати доступ до полів або методів підкласу, я повертаюсь до підкласу.

Я не хочу , щоб визначити однакові поля, як правило, використовується різних класах, як AudioSource/ Rigidbody/ Animatorі багато полів членів я визначив , як fireRate, damage, range, spreadі так далі. Також у деяких випадках деякі методи можуть бути перезаписані. Тому я використовую віртуальні методи в такому випадку, так що в основному я можу викликати цей метод, використовуючи метод із батьківського класу, але якщо логіка повинна бути різною у дочірнього, я їх вручну змінив.

Отже, чи варто відмовлятися від усіх цих речей як поганих практик? Чи варто використовувати замість цього інтерфейси?


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

3
Хто-небудь, хто запропонував вам інтерфейси, був кращим за спадщину, навів вам якісь аргументи? Мені цікаво.
Hawker65

1
@Cubic Я ніколи не говорив, що поліморфізм - це зразок. Я сказав: "... одна модель, якою мені дуже подобається, - це поліморфізм". Це означає, що візерунок, який мені справді подобається, використовує "поліморфізм", а не поліморфізм.
модернізатор

4
Люди балакають, використовуючи інтерфейси над успадкуванням у будь-якій формі розвитку. Однак це, як правило, додає багато накладних, когнітивних дисонансів, призводить до вибуху класу, часто додає сумнівної цінності і, як правило, не синергує добре в системі, якщо КОЖНІ в проекті не слідкують за цим фокусом на інтерфейсах. Тому не покидайте спадщину поки що. Однак ігри мають деякі унікальні характеристики, і архітектура Unity очікує, що ви будете використовувати парадигму дизайну CES, саме тому вам слід налаштувати свій підхід. Прочитайте серію "Чарівники та воїни" Еріка Ліпперта, що описує проблеми гри.
Данк

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

Відповіді:


65

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

Віддайте перевагу спадщині над складом у вашій логіці рівня додатків - від конструкцій інтерфейсу до служб. Наприклад,

Widget->Button->SpinnerButton або

ConnectionManager->TCPConnectionManagerпроти ->UDPConnectionManager.

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

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


10
Спадщина не означає, що всі методи були б віртуальними. Тож це автоматично не призводить до витрат на продуктивність. VTable відіграє лише роль у відправці віртуальних дзвінків, а не в доступі до членів даних.
Руслан

1
@ Руслан Я додав слова "може" і "можу", щоб вмістити ваш коментар. Інакше, в інтересах зберігати відповідь лаконічною, я не надто детально описувався. Спасибі.
Інженер

7
P.S. The other reason we may favour composition in entity systems is ... performance: Це справді правда? Переглядаючи сторінку WIkipedia, яку пов’язує @Linaith, показує, що вам потрібно буде скласти об'єкт інтерфейсів. Отже, у вас є (все-таки або навіть більше) віртуальні виклики функцій та більше пропусків кешу, коли ви ввели інший рівень непрямості.
Полум'я вогню

1
@Flamefire Так, це правда; Є способи впорядкувати свої дані таким чином, щоб ефективність кешу була оптимальною, незалежно, і, безумовно, набагато оптимальнішою, ніж ці додаткові непрямі. Вони не є успадкуванням і є композицією, хоча і більше в сенсі масив-структура / структура-масиви (переплетені / непереплетені дані в кеш-лінійному розташуванні), розбиваючись на окремі масиви і іноді дублюючи дані для відповідних наборів операцій у різних частинах вашого трубопроводу. Але знову ж таки, вдаватись до цього було б дуже серйозним відхиленням, якого найкраще уникати заради стислості.
Інженер

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

18

Ви вже отримали кілька приємних відповідей, але величезний слон у кімнаті у вашому запитанні такий:

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

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

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

Низька муфта

Це означає, що будь-яка «пачка речей» у вашому коді повинна якомога менше залежати від оточення. Це стосується класів (дизайн класу), але також об'єктів (фактична реалізація), "файлів" взагалі (тобто, кількість #includes на один .cppфайл, кількість importодного .javaфайлу тощо).

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

Спадщина збільшує зв'язок, очевидно; зміна базового класу змінює всі підкласи.

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

Висока згуртованість

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

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

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

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

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

Що ж тепер робити

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

Зрештою, схема, заснована на спадщині, все ще краща, ніж взагалі не має чіткого дизайну ООП. Тож сміливо дотримуйтесь цього. Якщо ви хочете, ви можете поскаржити / google трохи про низьку з’єднаність, високу згуртованість і побачити, чи хочете ви додати такого типу мислення до свого арсеналу. Ви завжди можете спробувати це випробувати, якщо хочете, пізніше; або спробуйте підходи, засновані на інтерфейсі, для наступного нового нового модуля коду.


Дякую за детальне пояснення. Це дійсно корисно зрозуміти, що я пропускаю.
модернізатор

13

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

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

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

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


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

3
@JamesTrotter У цей момент я думаю про Borderlands та його зброю, і дивуюсь, яка жахливість це була б лише у спадок ...
Baldrickk

1
@JamesTrotter Я хочу, щоб це була відповідь, а не коментар.
Фарап

6

Проблема полягає в тому, що успадкування призводить до з’єднання - ваші об'єкти повинні знати більше один про одного. Ось чому правило: "Завжди підтримуйте склад над спадщиною". Це не означає, що НІКОЛИ не використовуйте спадщину, це означає, що використовуйте її там, де це цілком доречно, але якщо ви коли-небудь сидите там, думаючи, "я можу це зробити обома способами, і вони обидва мають сенс", просто переходьте до композиції.

Спадщина також може бути обмежувальним. У вас є "Зброя", яка може бути AssultRifle - приголомшливо. Що станеться, коли у вас двоствольна рушниця і хочете дозволити стволам стріляти самостійно - трохи важче, але виконати, у вас просто клас з одноствольним та двоствольним стволом, але ви не можете просто встановити 2 одиночні бочки. на одній рушниці, може? Чи можете ви модифікувати 3 бочки або вам потрібен новий клас для цього?

А як щодо AssultRifle з гранатометом під ним - хм, трохи жорсткіше. Чи можете ви замінити GranadeLauncher ліхтариком для нічного полювання?

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

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

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


2
"як ви дозволяєте користувачеві робити попередні знаряддя, редагуючи файл конфігурації?" -> Зазвичай я створюю підсумковий клас для кожної зброї. Наприклад, як Glock17. Спочатку створіть клас WeaponBase. Цей клас абстрагується, визначає загальні значення, такі як режим пожежі, швидкість пожежі, розмір журналу, максимум боєприпасів, гранул. Крім того, він обробляє введення користувача, як mouse1 / mouse2 / reload та викликає відповідний метод. Вони майже віртуальні або розбиті на більше функцій, щоб розробник міг перекрити базову функціональність, якщо вони були потрібні. А потім створити ще один клас, який розширив WeaponBase,
модернатор

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

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

7
@modernator У Minecraft вони склали Monsterпідклас WalkingEntity. Потім вони додали шлейфи. Як ви гадаєте, що це спрацювало?
користувач253751

1
@immibis Чи є у вас посилання чи анекдотичне посилання на цю проблему? Ключові слова Googling Minecraft затоплюють мене у відеопосиланнях ;-)
Пітер - Відновити Моніку

3

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

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

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


Якщо інтерфейси є громадянами першого класу будь-якої мови ООП, мені цікаво, чи Python, Ruby,… не є мовами OOP.
BlackJack

@BlackJack: У Ruby інтерфейс є неявним (введення качки, якщо ви хочете), і виражається використанням складу замість спадкування (також, у нього є Mixins, які знаходяться десь посеред, наскільки я переймаюся) ...
AnoE

@BlackJack "Інтерфейс" (концепція) не вимагає interface(мовне ключове слово).
Калет

2
@Caleth Але називати їх першокласними громадянами робить ІМХО. Коли в мовному описі стверджується, що щось є громадянином першого класу, це зазвичай означає, що мова є справжньою мовою, яка має тип і значення і може передаватися навколо. Як і класи, це громадяни першого класу в Python, як і функції, методи та модулі. Якщо ми говоримо про концепцію, то інтерфейси також є частиною всіх мов, що не належать до програми OOP.
BlackJack

2

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

Спадщина проти складу - це питання - проти - є питання. Інтерфейси - це ще один (третій) спосіб, відповідний для деяких ситуацій.

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

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

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

Скажімо, у вас є асортименти, представлені класами, і вони мають функції, представлені функціями. Наприклад, у птаха були б Talk (), Fly () та Poop (). Клас Дак успадкував би від класу Bird, реалізуючи всі функції.

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

Якщо, однак, ви розділите функції на групи, представляючи кожну групу викликів за допомогою інтерфейсу, скажімо, IFly, що містить Takeoff (), FlapWings () та Land (), тоді ви могли б для класу Fox реалізувати функції з ITalk та IPoop але не IFly. Потім ви б визначили змінні та параметри, щоб прийняти об'єкти, що реалізують певний інтерфейс, і тоді код, який працює з ними, знав би, що він може викликати ... і завжди може запитувати інші інтерфейси, якщо йому потрібно перевірити, чи є також інші функції реалізований для поточного об'єкта.

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


1

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

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

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

В якості побічної записки я вважаю таке питання:

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

Спадкування не для повторного використання коду. Це для встановлення is-a відносини. Багато часу вони йдуть рука об руку, але треба бути обережним. Тільки тому, що, можливо, потрібно буде використовувати два модулі одного і того ж коду, це не означає, що один тип того ж типу, що й інший.

Ви можете використовувати інтерфейси, якщо хочете, скажімо, мати в ньому List<IWeapon>різні види зброї. Таким чином, ви можете успадкувати як інтерфейс IWeapon, так і підклас MonoBehaviour для всієї зброї, а також уникати будь-яких проблем з відсутністю багаторазового успадкування.


-3

Ви, здається, не розумієте, що таке інтерфейс. Мені знадобилося близько 10+ років, як я думав про ОО і задав питанням справді розумних людей, чи змогла я мати ах-ха момент з інтерфейсами. Сподіваюся, це допомагає.

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

Ця ж аналогія може бути використана між людиною (тілом і душею), що взаємодіє зі світом. Тіло - це інтерфейс між розумом і світом. Усі взаємодіють зі світом однаково, єдиною унікальністю є те, як ми взаємодіємо зі світом, саме так ми використовуємо наш МІНД, щоб вирішити, як ми інтерфейсуємо / взаємодіємо у світі.

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

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

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