Следует ли мне избегать использования наследования объектов при разработке игры?

36

Я предпочитаю функции ООП при разработке игр с Unity. Я обычно создаю базовый класс (в основном абстрагированный) и использую наследование объектов, чтобы использовать одни и те же функциональные возможности для других объектов.

Однако недавно я услышал от кого-то, что следует избегать использования наследования, и вместо этого мы должны использовать интерфейсы. Поэтому я спросил почему, и он сказал, что «Наследование объектов, конечно, важно, но если есть много расширенных объектов, отношения между классами тесно связаны.

Я использую абстрактный базовый класс, что - то вроде WeaponBaseи создания специальных классов оружия , как Shotgun, AssaultRifle, Machinegunчто - то подобное. Есть так много преимуществ, и один образец, которым я действительно наслаждаюсь, является полиморфизмом. Я могу рассматривать объект, созданный подклассами, как если бы они были базовым классом, так что логика может быть значительно сокращена и сделана более пригодной для повторного использования. Если мне нужно получить доступ к полям или методам подкласса, я возвращаюсь к подклассу.

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

Итак, следует ли отказаться от всего этого как от плохой практики? Должен ли я использовать интерфейсы вместо этого?

modernator
источник
30
«Есть так много преимуществ, и один образец, которым я действительно наслаждаюсь, это полиморфизм». Полиморфизм не шаблон. Это буквально весь смысл ООП. Другие вещи, такие как общие поля и т. Д., Могут быть легко достигнуты без дублирования кода или наследования.
Куб
3
Человек, предложивший вам интерфейсы, был лучше, чем наследование, дал вам какие-либо аргументы? Мне любопытно.
Hawker65
1
@ Кубик Я никогда не говорил, что полиморфизм - это паттерн. Я сказал: «... одна модель, которая мне действительно нравится, это полиморфизм». Это означает, что шаблон, который мне действительно нравится, использует «полиморфизм», а не полиморфизм - это шаблон.
модернист
4
Люди рекламируют использование интерфейсов по наследованию в любой форме разработки. Тем не менее, он имеет тенденцию добавлять много накладных, когнитивного диссонанса, приводит к взрыву класса, часто добавляет сомнительную ценность и обычно не дает синергизма в системе, если ВСЕ в проекте не следуют тому, что фокусируется на интерфейсах. Так что пока не отказывайтесь от наследства. Однако игры обладают некоторыми уникальными характеристиками, и архитектура Unity предполагает, что вы будете использовать парадигму дизайна CES, поэтому вам следует настроить свой подход. Прочитайте серию статей Эрика Липперта «Волшебники и воины», описывающую проблемы игрового типа.
Данк
2
Я бы сказал, что интерфейс - это частный случай наследования, когда базовый класс не содержит полей данных. Я думаю, что предложение состоит в том, чтобы попытаться не построить огромное дерево, где каждый объект должен быть классифицирован, а вместо этого множество маленьких деревьев, основанных на интерфейсах, где вы можете воспользоваться преимуществами множественного наследования. Полиморфизм не теряется с интерфейсами.
Эмануэле Паолини

Ответы:

65

Пользуйся композицией, а не наследованием в своей сущности и системах инвентаря / предметов. Этот совет имеет тенденцию применяться к игровым логическим структурам, когда способ, которым вы можете собрать сложные вещи (во время выполнения), может привести к множеству различных комбинаций; вот когда мы предпочитаем композицию.

Наслаждайтесь наследованием над композицией в логике уровня приложения - от конструкций пользовательского интерфейса до сервисов. Например,

Widget->Button->SpinnerButton или

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

... здесь есть четко определенная иерархия деривации, а не множество потенциальных дериваций, поэтому проще использовать наследование.

Итог : используйте наследование там, где можете, но используйте композиции там, где это необходимо. PS Другая причина, по которой мы можем предпочесть композицию в системах сущностей, состоит в том, что обычно существует много сущностей, и наследование может повлечь за собой затраты производительности для доступа к членам каждого объекта; смотри vtables .

инженер
источник
10
Наследование не подразумевает, что все методы будут виртуальными. Так что это автоматически не приводит к снижению производительности. VTable играет роль только в отправке виртуальных вызовов, а не в доступе к элементам данных.
Руслан
1
@Ruslan Я добавил слова «может» и «можно», чтобы учесть ваш комментарий. В противном случае в интересах краткости ответа я не стал вдаваться в подробности. Спасибо.
инженер
7
P.S. The other reason we may favour composition in entity systems is ... performance: Это действительно правда? Глядя на страницу WIkipedia, на которую ссылается @Linaith, видно, что вам нужно составить свой объект интерфейсов. Следовательно, у вас есть (все еще или даже больше) вызовы виртуальных функций и больше пропусков кэша, поскольку вы ввели другой уровень косвенности.
Flamefire
1
@ Flamefire Да, это правда; Существуют способы упорядочить ваши данные таким образом, чтобы эффективность кеша была оптимальной, независимо от того, и, конечно, гораздо более оптимальной, чем эти дополнительные косвенные действия Это не наследование и являются композициями, хотя и более в массиве из-структуры / структуры-из-массивов смысла ( с чередованием / без чередования данных в кэше-линейного расположении), разбиение на отдельные массивы , а иногда и дублирование данных , чтобы соответствовать наборам операций на разных участках вашего трубопровода. Но еще раз, вдаваться в это было бы очень серьезной диверсией, которую лучше всего избегать ради краткости.
инженер
2
Пожалуйста, не забывайте оставлять комментарии к гражданским запросам о разъяснении или критике и обсуждать содержание идей, а не характер или опыт человека , который их высказывает.
Джош
18

Вы уже получили несколько хороших ответов, но вот огромный слон в комнате в вашем вопросе:

слышал от кого-то, что следует избегать использования наследования, и вместо этого мы должны использовать интерфейсы

Как правило, когда кто-то дает вам правило, игнорируйте его. Это касается не только «того, кто вам что-то говорит», но и чтения материалов в Интернете. Если вы не знаете, почему (и действительно можете это поддержать), такой совет бесполезен и часто очень вреден.

По моему опыту, наиболее важными и полезными концепциями в ООП являются «слабая связь» и «высокая сплоченность» (классы / объекты знают как можно меньше друг о друге, и каждая единица отвечает за как можно меньше вещей).

Низкая связь

Это означает, что любой «набор вещей» в вашем коде должен как можно меньше зависеть от его окружения. Это относится как к классам (дизайн классов), так и к объектам (фактическая реализация), к «файлам» в целом (т. Е. К числу #includes в одном .cppфайле, к числу файлов importв одном .javaфайле и т. Д.).

Признаком того, что две сущности связаны, является то, что одна из них сломается (или должна быть изменена), когда другая изменится каким-либо образом.

Наследование увеличивает сцепление, очевидно; изменение базового класса изменяет все подклассы.

Интерфейсы уменьшают связь: определяя четкий контракт на основе методов, вы можете свободно изменять что-либо в обеих сторонах интерфейса, если вы не измените контракт. (Обратите внимание, что «интерфейс» - это общая концепция, interfaceабстрактные классы Java или C ++ - это просто детали реализации).

Высокая сплоченность

Это означает, что каждый класс, объект, файл и т. Д. Должны быть связаны или отвечать за как можно меньше. Т.е. избегайте больших классов, которые делают много вещей. В вашем примере, если ваше оружие имеет совершенно разные аспекты (боеприпасы, поведение при стрельбе, графическое представление, представление инвентаря и т. Д.), То вы можете иметь разные классы, которые представляют именно одну из этих вещей. Основной класс оружия затем превращается в «держателя» этих деталей; тогда объект оружия - это всего лишь несколько указателей на эти детали.

В этом примере вы должны убедиться, что ваш класс, представляющий «поведение при стрельбе», знает как можно меньше о главном классе оружия. Оптимально, вообще ничего. Это может означать, например, что вы можете дать «Поведение огня» любому объекту в вашем мире (башенкам, вулканам, неигровым персонажам ...) одним щелчком пальца. Если вы в какой-то момент захотите изменить способ представления оружия в инвентаре, вы можете просто сделать это - об этом знает только ваш класс инвентаря.

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

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

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

что делать сейчас

Там до сих пор нет жестких и быстрых правил, все это только рекомендации. В целом, как отметил пользователь TKK в своем ответе, наследование многому учат в школе и книгах; это модные вещи об ООП. Интерфейсы, вероятно, более скучны в обучении, а также (если вы пропустите тривиальные примеры) немного сложнее, открывая область внедрения зависимостей, которая не так очевидна, как наследование.

В конце концов, ваша схема, основанная на наследовании, все же лучше, чем отсутствие четкой схемы ООП вообще. Так что не стесняйтесь придерживаться этого. Если вы хотите, вы можете поразмышлять / погуглить немного о Low Coupling, High Cohesion и посмотреть, хотите ли вы добавить такой тип мышления в свой арсенал. Вы всегда можете рефакторинг, чтобы попробовать это, если хотите, позже; или опробуйте подходы, основанные на интерфейсе, на следующем крупном модуле кода.

Anoe
источник
Спасибо за подробное объяснение. Это действительно полезно понять, что я скучаю.
модернист
13

Идея, что наследства следует избегать, просто ошибочна.

Существует принцип кодирования под названием « Композиция по наследованию» . Это говорит о том, что вы можете достичь того же с помощью композиции, и это предпочтительнее, потому что вы можете повторно использовать часть кода. См. Почему я должен предпочесть композицию наследованию?

Я должен сказать, что мне нравятся ваши классы оружия, и будет ли это так же. Но я еще не сделал игру ...

Как отметил Джеймс Троттер, у композиции могут быть некоторые преимущества, особенно в гибкости во время выполнения, чтобы изменить работу оружия. Это было бы возможно с наследованием, но это сложнее.

Linaith
источник
14
Вместо того, чтобы иметь классы для оружия, вы могли бы иметь один класс «оружия» и компоненты для обработки того, что делает стрельба, какие у него есть вложения и что они делают, какое у него «хранилище боеприпасов», вы можете создать много конфигураций. некоторые из которых могут быть «дробовиком» или «штурмовой винтовкой», но их также можно изменить во время выполнения, например, для переключения навесного оборудования, изменения емкости магазина и т. д. Или могут быть созданы целые новые конфигурации пистолетов, которые не были даже не задумывался изначально. Да, вы можете достичь чего-то похожего с наследованием, но не так просто.
Trotski94
3
@JamesTrotter В этот момент я думаю о Borderlands и его оружии, и удивляюсь, что это за чудовище только с наследством ...
Baldrickk
1
@JamesTrotter Я бы хотел, чтобы это был ответ, а не комментарий.
Pharap
6

Проблема в том, что наследование ведет к связыванию - ваши объекты должны знать больше друг о друге. Вот почему правило «Всегда отдавай предпочтение композиции, а не наследованию». Это не означает, что НИКОГДА не использовать наследование, это означает использовать его там, где это абсолютно уместно, но если вы когда-нибудь сидите и думаете: «Я мог бы сделать это в обоих направлениях, и они оба имеют смысл», просто переходите прямо к композиции.

Наследование также может быть своего рода ограничением. У вас есть "WeaponBase", которая может быть AssultRifle - круто. Что происходит, когда у вас есть двуствольное ружье и вы хотите, чтобы стволы стреляли независимо - немного сложнее, но выполнимо, у вас просто класс с одним стволом и двумя стволами, но вы не можете просто установить 2 одиночных ствола на том же пистолете, не так ли? Можете ли вы изменить один, чтобы иметь 3 бочки, или вам нужен новый класс для этого?

Как насчет AssultRifle с гранатометом под ним - хм, немного жестче. Можете ли вы заменить GrenadeLauncher фонариком для ночной охоты?

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

Множественное наследование может несколько исправить некоторые из этих тривиальных примеров, но оно добавляет свой собственный набор проблем.

До того, как появились общепринятые высказывания, такие как «Предпочитать композицию над наследованием», я узнал об этом, написав слишком сложное дерево наследования (что было потрясающе весело и прекрасно работало), а затем обнаружил, что его действительно трудно изменить и поддерживать позже. Поговорка такова, что у вас есть альтернативный и компактный способ выучить и запомнить такую ​​концепцию без необходимости проходить через весь опыт - но если вы хотите интенсивно использовать наследование, я рекомендую просто сохранять открытость и оценивать, насколько хорошо это работает для вас - ничего страшного не произойдет, если вы будете интенсивно использовать наследование, и в худшем случае это может быть отличным опытом обучения (в лучшем случае это может сработать для вас)

Билл К
источник
2
"Как вы позволяете своему пользователю создавать предыдущие пушки, редактируя файл конфигурации?" -> Шаблон, который я обычно делаю, - это создание финального класса для каждого оружия. Например, как Glock17. Сначала создайте класс WeaponBase. Этот класс является абстрагированным, определяет общие значения, такие как режим стрельбы, скорострельность, размер магазина, максимальные боеприпасы, гранулы. Также он обрабатывает пользовательский ввод, такой как mouse1 / mouse2 / reload, и вызывает соответствующий метод. Они почти виртуальны или разделены на большее количество функций, так что разработчик может переопределить базовую функциональность, если это необходимо. А затем создайте еще один класс, который расширил базу оружия,
модернист
2
что-то вроде SlideStoppableWeapon. У большинства пистолетов есть это, и это обрабатывает дополнительные поведения, такие как остановка скольжения. Наконец, создайте класс Glock17, который расширяется от SlideStoppableWeapon. Glock17 - последний класс, если у оружия есть уникальная способность, которая только что, наконец, написана здесь. Я обычно использую этот шаблон, когда у пистолета есть специальный пожарный или перезарядный механизм. Реализуйте это уникальное поведение до конца иерархии классов, максимально комбинируйте общие логики от родителей.
модернист
2
@modernator Как я уже сказал, это всего лишь руководство - если вы не доверяете этому, не стесняйтесь игнорировать его, просто следите за результатами в течение долгого времени и оценивайте преимущества от боли время от времени. Я пытаюсь вспомнить места, где я ущипнул себя, и помогаю другим избегать того же, но то, что работает для меня, может не сработать для вас.
Билл К
7
@modernator В Minecraft они сделали Monsterподкласс WalkingEntity. Затем они добавили слизи. Насколько хорошо вы думаете, что это сработало?
user253751
1
@immibis У вас есть ссылка или анекдотическая ссылка на эту проблему? Поиск в Google по ключевым словам в Minecraft топит меня по ссылкам на видео ;-)
Питер - восстанови Монику
3

В отличие от других ответов, это не имеет ничего общего с наследованием против композиции. Наследование или состав - это решение, которое вы принимаете относительно того, как будет реализован класс. Интерфейсы против классов - это решение, которое приходит раньше.

Интерфейсы - это первоклассные граждане любого языка ООП. Классы - это второстепенные детали реализации. Умы новых программистов сильно искажены учителями и книгами, которые вводят классы и наследование перед интерфейсами.

Ключевой принцип состоит в том, что, когда это возможно, типы всех параметров метода и возвращаемых значений должны быть интерфейсами, а не классами. Это делает ваши API намного более гибкими и мощными. В подавляющем большинстве случаев ваши методы и вызывающие их лица не должны иметь оснований знать или заботиться о реальных классах объектов, с которыми они имеют дело. Если ваш API не зависит от деталей реализации, вы можете свободно переключаться между составом и наследованием, ничего не нарушая.

Нет U
источник
Если интерфейсы являются первоклассными гражданами любого языка ООП, мне интересно, не являются ли Python, Ruby, ... языками ООП.
Блэкджек,
@BlackJack: В Ruby интерфейс неявный (если хотите, дак-типинг), и выражается с помощью композиции вместо наследования (также, как я понимаю, он имеет Mixins, которые находятся где-то посередине) ...
AnoE
@ BlackJack «Интерфейс» (концепция) не требуется interface(ключевое слово языка).
Caleth
2
@Caleth Но называть их первоклассными гражданами - ИМХО. Когда в описании языка утверждается, что что-то является гражданином первого класса, это обычно означает, что в этом языке есть нечто реальное, которое имеет тип и значение и может быть передано. Подобные классы являются гражданами первого класса в Python, так же как и функции, методы и модули. Если мы говорим о концепции, тогда интерфейсы являются частью всех не-ООП языков.
Блэкджек,
2

Всякий раз, когда кто-то говорит вам, что один конкретный подход является лучшим для всех случаев, это то же самое, что говорить вам, что одно и то же лекарство лечит все болезни.

Наследование против композиции - это вопрос "есть против". Интерфейсы - это еще один (3-й) способ, подходящий для некоторых ситуаций.

Наследование, или логика is-a: вы используете его, когда новый класс будет вести себя, и будете использоваться полностью (внешне) как старый класс, из которого вы производите его, если новый класс будет представлен публике все функции, которые были у старого класса ... тогда вы наследуете.

Композиция или логика has-a: если новому классу просто нужно внутренне использовать старый класс, не раскрывая возможности старого класса, то вы используете композицию, то есть экземпляр старого класса как свойство member или переменная нового. (Это может быть частная собственность, или защищенная, или любая другая, в зависимости от варианта использования). Ключевым моментом здесь является то, что это тот случай использования, когда вы не хотите раскрывать исходные функции класса и использовать их для публики, просто используйте его для внутреннего использования, в то время как в случае наследования вы предоставляете их публике через новый класс. Иногда вам нужен один подход, а иногда другой.

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

Допустим, у вас есть разные существа, представленные классами, и у них есть функции, представленные функциями. Например, птица будет иметь Talk (), Fly () и Poop (). Класс Duck наследуется от класса Bird, реализуя все функции.

Класс Фокс, очевидно, не может летать. Поэтому, если вы определите, что у предка есть все функции, вы не сможете получить потомка должным образом.

Однако, если вы разбиваете функции на группы, представляющие каждую группу вызовов с интерфейсом, скажем, IFly, содержащим Takeoff (), FlapWings () и Land (), то вы могли бы для класса Fox реализовать функции из ITalk и IPoop но не только Затем вы должны определить переменные и параметры для принятия объектов, которые реализуют определенный интерфейс, а затем код, работающий с ними, будет знать, что он может вызывать ... и всегда сможет запрашивать другие интерфейсы, если ему нужно увидеть, есть ли другие функциональные возможности. реализовано для текущего объекта.

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

Драган Юрич
источник
1

Для игры, особенно с Unity, которая работает с архитектурой сущностных компонентов, вы должны отдавать предпочтение композиции, а не наследованию ваших игровых компонентов. Он гораздо более гибкий и позволяет избежать попадания в ситуацию, когда вы хотите, чтобы игровая сущность «была» двумя вещами, находящимися на разных листьях дерева наследования.

Например, допустим, у вас есть TalkingAI в одной ветви дерева наследования, а VolumetricCloud - в другой отдельной ветви, и вам нужно говорящее облако. Это сложно с глубоким наследованием дерева. С помощью компонент-сущности вы просто создаете сущность, в которой есть компоненты TalkingAI и Cloud, и все готово.

Но это не означает, что, скажем, в вашей реализации объемного облака вы не должны использовать наследование. Код для этого может быть содержательным и состоять из нескольких классов, и вы можете использовать ООП по мере необходимости. Но все это будет составлять один игровой компонент.

Как примечание стороны, я беру некоторую проблему с этим:

Я обычно создаю базовый класс (в основном абстрагированный) и использую наследование объектов, чтобы использовать одни и те же функциональные возможности для других объектов.

Наследование не для повторного использования кода. Это для создания есть- отношения. В большинстве случаев они идут рука об руку, но вы должны быть осторожны. То, что двум модулям может понадобиться использовать один и тот же код, не означает, что один из них имеет тот же тип, что и другой.

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

Сава Б.
источник
-3

Вы, кажется, не понимаете, что интерфейс или делает. У меня ушло более 10 лет на размышления об ОО и задание некоторых действительно умных вопросов о том, смогу ли я получить момент ах-ха с интерфейсами. Надеюсь это поможет.

Лучше всего использовать их комбинацию. По моему мнению, моделирование мира и людей, скажем, один глубокий пример. Душа связана с телом через разум. Разум - это интерфейс между душой (некоторые считают интеллектом) и командами, которые он дает телу. Функционально говоря, умы взаимодействуют с телом одинаково для всех людей.

Та же самая аналогия может быть использована между человеком (телом и душой), взаимодействующим с миром. Тело - это интерфейс между разумом и миром. Все взаимодействуют с миром одинаково, единственная уникальность в том, КАК мы взаимодействуем с миром, то есть, как мы используем УМ, чтобы решить, как мы взаимодействуем / взаимодействуем в мире.

Интерфейс - это просто механизм, чтобы связать вашу структуру ОО с другой структурой ОО, при этом каждая сторона имеет разные атрибуты, которые должны согласовываться / сопоставляться друг с другом для получения некоторого функционального выхода в другой среде / среде.

Таким образом, для того, чтобы разум мог вызывать функцию с открытыми руками, он должен реализовать интерфейс нервной_команды_системы с ТА, имеющим собственный API с инструкциями, как реализовать все требования, необходимые перед вызовом с открытыми руками.

Кристофер Хоффман
источник