Я предпочитаю функции ООП при разработке игр с Unity. Я обычно создаю базовый класс (в основном абстрагированный) и использую наследование объектов, чтобы использовать одни и те же функциональные возможности для других объектов.
Однако недавно я услышал от кого-то, что следует избегать использования наследования, и вместо этого мы должны использовать интерфейсы. Поэтому я спросил почему, и он сказал, что «Наследование объектов, конечно, важно, но если есть много расширенных объектов, отношения между классами тесно связаны.
Я использую абстрактный базовый класс, что - то вроде WeaponBase
и создания специальных классов оружия , как Shotgun
, AssaultRifle
, Machinegun
что - то подобное. Есть так много преимуществ, и один образец, которым я действительно наслаждаюсь, является полиморфизмом. Я могу рассматривать объект, созданный подклассами, как если бы они были базовым классом, так что логика может быть значительно сокращена и сделана более пригодной для повторного использования. Если мне нужно получить доступ к полям или методам подкласса, я возвращаюсь к подклассу.
Я не хочу , чтобы определить одинаковые поля, обычно используемые в разных классах, как AudioSource
/ Rigidbody
/ Animator
и много полей членов я определил , как fireRate
, damage
, range
, spread
и так далее. Также в некоторых случаях некоторые методы могут быть перезаписаны. Так что в этом случае я использую виртуальные методы, так что в основном я могу вызвать этот метод, используя метод из родительского класса, но если логика должна быть другой в дочернем, я вручную переопределил их.
Итак, следует ли отказаться от всего этого как от плохой практики? Должен ли я использовать интерфейсы вместо этого?
источник
Ответы:
Пользуйся композицией, а не наследованием в своей сущности и системах инвентаря / предметов. Этот совет имеет тенденцию применяться к игровым логическим структурам, когда способ, которым вы можете собрать сложные вещи (во время выполнения), может привести к множеству различных комбинаций; вот когда мы предпочитаем композицию.
Наслаждайтесь наследованием над композицией в логике уровня приложения - от конструкций пользовательского интерфейса до сервисов. Например,
Widget->Button->SpinnerButton
илиConnectionManager->TCPConnectionManager
против->UDPConnectionManager
.... здесь есть четко определенная иерархия деривации, а не множество потенциальных дериваций, поэтому проще использовать наследование.
Итог : используйте наследование там, где можете, но используйте композиции там, где это необходимо. PS Другая причина, по которой мы можем предпочесть композицию в системах сущностей, состоит в том, что обычно существует много сущностей, и наследование может повлечь за собой затраты производительности для доступа к членам каждого объекта; смотри vtables .
источник
P.S. The other reason we may favour composition in entity systems is ... performance
: Это действительно правда? Глядя на страницу WIkipedia, на которую ссылается @Linaith, видно, что вам нужно составить свой объект интерфейсов. Следовательно, у вас есть (все еще или даже больше) вызовы виртуальных функций и больше пропусков кэша, поскольку вы ввели другой уровень косвенности.Вы уже получили несколько хороших ответов, но вот огромный слон в комнате в вашем вопросе:
Как правило, когда кто-то дает вам правило, игнорируйте его. Это касается не только «того, кто вам что-то говорит», но и чтения материалов в Интернете. Если вы не знаете, почему (и действительно можете это поддержать), такой совет бесполезен и часто очень вреден.
По моему опыту, наиболее важными и полезными концепциями в ООП являются «слабая связь» и «высокая сплоченность» (классы / объекты знают как можно меньше друг о друге, и каждая единица отвечает за как можно меньше вещей).
Низкая связь
Это означает, что любой «набор вещей» в вашем коде должен как можно меньше зависеть от его окружения. Это относится как к классам (дизайн классов), так и к объектам (фактическая реализация), к «файлам» в целом (т. Е. К числу
#include
s в одном.cpp
файле, к числу файловimport
в одном.java
файле и т. Д.).Признаком того, что две сущности связаны, является то, что одна из них сломается (или должна быть изменена), когда другая изменится каким-либо образом.
Наследование увеличивает сцепление, очевидно; изменение базового класса изменяет все подклассы.
Интерфейсы уменьшают связь: определяя четкий контракт на основе методов, вы можете свободно изменять что-либо в обеих сторонах интерфейса, если вы не измените контракт. (Обратите внимание, что «интерфейс» - это общая концепция,
interface
абстрактные классы Java или C ++ - это просто детали реализации).Высокая сплоченность
Это означает, что каждый класс, объект, файл и т. Д. Должны быть связаны или отвечать за как можно меньше. Т.е. избегайте больших классов, которые делают много вещей. В вашем примере, если ваше оружие имеет совершенно разные аспекты (боеприпасы, поведение при стрельбе, графическое представление, представление инвентаря и т. Д.), То вы можете иметь разные классы, которые представляют именно одну из этих вещей. Основной класс оружия затем превращается в «держателя» этих деталей; тогда объект оружия - это всего лишь несколько указателей на эти детали.
В этом примере вы должны убедиться, что ваш класс, представляющий «поведение при стрельбе», знает как можно меньше о главном классе оружия. Оптимально, вообще ничего. Это может означать, например, что вы можете дать «Поведение огня» любому объекту в вашем мире (башенкам, вулканам, неигровым персонажам ...) одним щелчком пальца. Если вы в какой-то момент захотите изменить способ представления оружия в инвентаре, вы можете просто сделать это - об этом знает только ваш класс инвентаря.
Признаком того, что сущность не является связной, является то, что она становится все больше и больше, разветвляясь в нескольких направлениях одновременно.
Наследование, как вы его описываете, снижает сплоченность - ваши классы оружия, в конце концов, являются большими кусками, которые обрабатывают все виды различных, не связанных аспектов вашего оружия.
Интерфейсы косвенно повышают согласованность, четко разделяя обязанности между двумя сторонами интерфейса.
что делать сейчас
Там до сих пор нет жестких и быстрых правил, все это только рекомендации. В целом, как отметил пользователь TKK в своем ответе, наследование многому учат в школе и книгах; это модные вещи об ООП. Интерфейсы, вероятно, более скучны в обучении, а также (если вы пропустите тривиальные примеры) немного сложнее, открывая область внедрения зависимостей, которая не так очевидна, как наследование.
В конце концов, ваша схема, основанная на наследовании, все же лучше, чем отсутствие четкой схемы ООП вообще. Так что не стесняйтесь придерживаться этого. Если вы хотите, вы можете поразмышлять / погуглить немного о Low Coupling, High Cohesion и посмотреть, хотите ли вы добавить такой тип мышления в свой арсенал. Вы всегда можете рефакторинг, чтобы попробовать это, если хотите, позже; или опробуйте подходы, основанные на интерфейсе, на следующем крупном модуле кода.
источник
Идея, что наследства следует избегать, просто ошибочна.
Существует принцип кодирования под названием « Композиция по наследованию» . Это говорит о том, что вы можете достичь того же с помощью композиции, и это предпочтительнее, потому что вы можете повторно использовать часть кода. См. Почему я должен предпочесть композицию наследованию?
Я должен сказать, что мне нравятся ваши классы оружия, и будет ли это так же. Но я еще не сделал игру ...Как отметил Джеймс Троттер, у композиции могут быть некоторые преимущества, особенно в гибкости во время выполнения, чтобы изменить работу оружия. Это было бы возможно с наследованием, но это сложнее.
источник
Проблема в том, что наследование ведет к связыванию - ваши объекты должны знать больше друг о друге. Вот почему правило «Всегда отдавай предпочтение композиции, а не наследованию». Это не означает, что НИКОГДА не использовать наследование, это означает использовать его там, где это абсолютно уместно, но если вы когда-нибудь сидите и думаете: «Я мог бы сделать это в обоих направлениях, и они оба имеют смысл», просто переходите прямо к композиции.
Наследование также может быть своего рода ограничением. У вас есть "WeaponBase", которая может быть AssultRifle - круто. Что происходит, когда у вас есть двуствольное ружье и вы хотите, чтобы стволы стреляли независимо - немного сложнее, но выполнимо, у вас просто класс с одним стволом и двумя стволами, но вы не можете просто установить 2 одиночных ствола на том же пистолете, не так ли? Можете ли вы изменить один, чтобы иметь 3 бочки, или вам нужен новый класс для этого?
Как насчет AssultRifle с гранатометом под ним - хм, немного жестче. Можете ли вы заменить GrenadeLauncher фонариком для ночной охоты?
Наконец, как вы позволяете вашему пользователю создавать предыдущие пушки, редактируя файл конфигурации? Это сложно, потому что у вас есть жестко запрограммированные отношения, которые могут быть лучше составлены и настроены с данными.
Множественное наследование может несколько исправить некоторые из этих тривиальных примеров, но оно добавляет свой собственный набор проблем.
До того, как появились общепринятые высказывания, такие как «Предпочитать композицию над наследованием», я узнал об этом, написав слишком сложное дерево наследования (что было потрясающе весело и прекрасно работало), а затем обнаружил, что его действительно трудно изменить и поддерживать позже. Поговорка такова, что у вас есть альтернативный и компактный способ выучить и запомнить такую концепцию без необходимости проходить через весь опыт - но если вы хотите интенсивно использовать наследование, я рекомендую просто сохранять открытость и оценивать, насколько хорошо это работает для вас - ничего страшного не произойдет, если вы будете интенсивно использовать наследование, и в худшем случае это может быть отличным опытом обучения (в лучшем случае это может сработать для вас)
источник
Monster
подклассWalkingEntity
. Затем они добавили слизи. Насколько хорошо вы думаете, что это сработало?В отличие от других ответов, это не имеет ничего общего с наследованием против композиции. Наследование или состав - это решение, которое вы принимаете относительно того, как будет реализован класс. Интерфейсы против классов - это решение, которое приходит раньше.
Интерфейсы - это первоклассные граждане любого языка ООП. Классы - это второстепенные детали реализации. Умы новых программистов сильно искажены учителями и книгами, которые вводят классы и наследование перед интерфейсами.
Ключевой принцип состоит в том, что, когда это возможно, типы всех параметров метода и возвращаемых значений должны быть интерфейсами, а не классами. Это делает ваши API намного более гибкими и мощными. В подавляющем большинстве случаев ваши методы и вызывающие их лица не должны иметь оснований знать или заботиться о реальных классах объектов, с которыми они имеют дело. Если ваш API не зависит от деталей реализации, вы можете свободно переключаться между составом и наследованием, ничего не нарушая.
источник
interface
(ключевое слово языка).Всякий раз, когда кто-то говорит вам, что один конкретный подход является лучшим для всех случаев, это то же самое, что говорить вам, что одно и то же лекарство лечит все болезни.
Наследование против композиции - это вопрос "есть против". Интерфейсы - это еще один (3-й) способ, подходящий для некоторых ситуаций.
Наследование, или логика is-a: вы используете его, когда новый класс будет вести себя, и будете использоваться полностью (внешне) как старый класс, из которого вы производите его, если новый класс будет представлен публике все функции, которые были у старого класса ... тогда вы наследуете.
Композиция или логика has-a: если новому классу просто нужно внутренне использовать старый класс, не раскрывая возможности старого класса, то вы используете композицию, то есть экземпляр старого класса как свойство member или переменная нового. (Это может быть частная собственность, или защищенная, или любая другая, в зависимости от варианта использования). Ключевым моментом здесь является то, что это тот случай использования, когда вы не хотите раскрывать исходные функции класса и использовать их для публики, просто используйте его для внутреннего использования, в то время как в случае наследования вы предоставляете их публике через новый класс. Иногда вам нужен один подход, а иногда другой.
Интерфейсы: Интерфейсы предназначены для еще одного варианта использования - когда вы хотите, чтобы новый класс частично, а не полностью реализовывал и предоставлял публике функциональность старого. Это позволяет вам иметь новый класс, класс из совершенно другой иерархии от старого, вести себя как старый только в некоторых аспектах.
Допустим, у вас есть разные существа, представленные классами, и у них есть функции, представленные функциями. Например, птица будет иметь Talk (), Fly () и Poop (). Класс Duck наследуется от класса Bird, реализуя все функции.
Класс Фокс, очевидно, не может летать. Поэтому, если вы определите, что у предка есть все функции, вы не сможете получить потомка должным образом.
Однако, если вы разбиваете функции на группы, представляющие каждую группу вызовов с интерфейсом, скажем, IFly, содержащим Takeoff (), FlapWings () и Land (), то вы могли бы для класса Fox реализовать функции из ITalk и IPoop но не только Затем вы должны определить переменные и параметры для принятия объектов, которые реализуют определенный интерфейс, а затем код, работающий с ними, будет знать, что он может вызывать ... и всегда сможет запрашивать другие интерфейсы, если ему нужно увидеть, есть ли другие функциональные возможности. реализовано для текущего объекта.
У каждого из этих подходов есть варианты использования, когда он самый лучший, и ни один подход не является абсолютно лучшим решением для всех случаев.
источник
Для игры, особенно с Unity, которая работает с архитектурой сущностных компонентов, вы должны отдавать предпочтение композиции, а не наследованию ваших игровых компонентов. Он гораздо более гибкий и позволяет избежать попадания в ситуацию, когда вы хотите, чтобы игровая сущность «была» двумя вещами, находящимися на разных листьях дерева наследования.
Например, допустим, у вас есть TalkingAI в одной ветви дерева наследования, а VolumetricCloud - в другой отдельной ветви, и вам нужно говорящее облако. Это сложно с глубоким наследованием дерева. С помощью компонент-сущности вы просто создаете сущность, в которой есть компоненты TalkingAI и Cloud, и все готово.
Но это не означает, что, скажем, в вашей реализации объемного облака вы не должны использовать наследование. Код для этого может быть содержательным и состоять из нескольких классов, и вы можете использовать ООП по мере необходимости. Но все это будет составлять один игровой компонент.
Как примечание стороны, я беру некоторую проблему с этим:
Наследование не для повторного использования кода. Это для создания есть- отношения. В большинстве случаев они идут рука об руку, но вы должны быть осторожны. То, что двум модулям может понадобиться использовать один и тот же код, не означает, что один из них имеет тот же тип, что и другой.
Вы можете использовать интерфейсы, если хотите, скажем, иметь
List<IWeapon>
различные типы оружия. Таким образом, вы можете наследовать как от интерфейса IWeapon, так и от подкласса MonoBehaviour для всех видов оружия, и избежать любых проблем с отсутствием множественного наследования.источник
Вы, кажется, не понимаете, что интерфейс или делает. У меня ушло более 10 лет на размышления об ОО и задание некоторых действительно умных вопросов о том, смогу ли я получить момент ах-ха с интерфейсами. Надеюсь это поможет.
Лучше всего использовать их комбинацию. По моему мнению, моделирование мира и людей, скажем, один глубокий пример. Душа связана с телом через разум. Разум - это интерфейс между душой (некоторые считают интеллектом) и командами, которые он дает телу. Функционально говоря, умы взаимодействуют с телом одинаково для всех людей.
Та же самая аналогия может быть использована между человеком (телом и душой), взаимодействующим с миром. Тело - это интерфейс между разумом и миром. Все взаимодействуют с миром одинаково, единственная уникальность в том, КАК мы взаимодействуем с миром, то есть, как мы используем УМ, чтобы решить, как мы взаимодействуем / взаимодействуем в мире.
Интерфейс - это просто механизм, чтобы связать вашу структуру ОО с другой структурой ОО, при этом каждая сторона имеет разные атрибуты, которые должны согласовываться / сопоставляться друг с другом для получения некоторого функционального выхода в другой среде / среде.
Таким образом, для того, чтобы разум мог вызывать функцию с открытыми руками, он должен реализовать интерфейс нервной_команды_системы с ТА, имеющим собственный API с инструкциями, как реализовать все требования, необходимые перед вызовом с открытыми руками.
источник