Каковы преимущества ООП на основе прототипов перед ООП на основе классов?


47

Когда я впервые начал программировать Javascript после того, как в основном работал с ООП в контексте языков, основанных на классах, я был озадачен тем, почему ООП на основе прототипов когда-либо предпочтительнее ООП на основе классов.

  1. Каковы структурные преимущества использования ООП на основе прототипа, если таковые имеются? (Например, можем ли мы ожидать, что это будет быстрее или менее интенсивно в некоторых приложениях?)
  2. Каковы преимущества с точки зрения кодировщика? (Например, проще ли кодировать определенные приложения или расширять код других людей с помощью прототипирования?)

Пожалуйста, не рассматривайте этот вопрос как вопрос о Javascript в частности (в течение многих лет у него было много недостатков, совершенно не связанных с прототипированием). Вместо этого, пожалуйста, посмотрите на это в контексте теоретических преимуществ прототипирования по сравнению с классами.

Спасибо.


Ответы:


46

У меня было довольно много опыта обоих подходов при написании RPG-игры на Java. Первоначально я написал всю игру, используя ООП на основе классов, но в конце концов понял, что это был неправильный подход (он становился неосуществимым по мере расширения иерархии классов). Поэтому я преобразовал всю кодовую базу в прототипный код. Результат оказался намного лучше и проще в управлении.

Исходный код здесь, если вы заинтересованы ( Тиран - Java Roguelike )

Вот основные преимущества:

  • Создать новые «классы» тривиально - просто скопируйте прототип и измените пару свойств и вуаля ... новый класс. Я использовал это, чтобы определить новый тип зелья, например, в 3-6 строках Java. Гораздо лучше, чем новый файл класса и множество шаблонов!
  • Можно создать и поддерживать чрезвычайно большое количество «классов» с помощью сравнительно небольшого кода - например, у Тирана было что-то вроде 3000 различных прототипов с общим количеством строк кода около 42 000. Это довольно удивительно для Java!
  • Многократное наследование легко - вы просто копируете подмножество свойств из одного прототипа и вставляете их в свойства другого прототипа. Например, в RPG вам может понадобиться, чтобы «стальной голем» имел некоторые свойства «стального объекта», а некоторые свойства «голема» и некоторые свойства «неразумного монстра». Просто с прототипами, попробуйте сделать это с наследственной наследственностью ......
  • Вы можете делать умные вещи с помощью модификаторов свойств - помещая умную логику в общий метод «чтения свойств», вы можете реализовать различные модификаторы. Например, было легко определить магическое кольцо, которое добавляло +2 силы тому, кто его носил. Логика для этого была в объекте ring, а не в методе «сила чтения», поэтому вы избежали необходимости помещать множество условных тестов где-то еще в вашей базе кода (например, «носит ли персонаж увеличение кольца силы?»)
  • Экземпляры могут стать шаблонами для других экземпляров - например, если вы хотите «клонировать» объект, это легко, просто используйте существующий объект в качестве прототипа для нового объекта. Не нужно писать много сложной клонирующей логики для разных классов.
  • Очень легко изменить поведение во время выполнения - то есть вы можете изменить свойства и «изменить» объект практически произвольно во время выполнения. Позволяет создавать крутые внутриигровые эффекты, и если вы соедините это с «языком сценариев», то практически все возможно во время выполнения.
  • Это больше подходит для "функционального" стиля программирования - вы склонны писать множество функций, которые анализируют объекты соответствующим образом, а не встроенную логику в методах, привязанных к конкретным классам. Я лично предпочитаю этот стиль FP.

Вот основные недостатки:

  • Вы теряете гарантии статической типизации - поскольку вы эффективно создаете динамическую объектную систему. Это означает, что вам нужно написать больше тестов, чтобы убедиться, что поведение правильное, а объекты правильного «вида».
  • Это приводит к некоторому снижению производительности - поскольку при чтении свойств объекта обычно требуется выполнить один или несколько поисков карты, вы платите небольшую цену с точки зрения производительности. В моем случае это не было проблемой, но в некоторых случаях это могло быть проблемой (например, 3D FPS с большим количеством объектов, запрашиваемых в каждом кадре)
  • Рефакторинг не работает одинаково - в системе, основанной на прототипах, вы, по сути, «строите» свою иерархию наследования с помощью кода. Среды IDE / инструменты рефакторинга не могут вам помочь, так как они не могут обмануть ваш подход. Я никогда не считал это проблемой, но она может выйти из-под контроля, если вы не будете осторожны. Вы, вероятно, хотите, чтобы тесты проверяли, что ваша иерархия наследования строится правильно!
  • Это немного чуждо - люди, привыкшие к традиционному стилю ООП, могут легко запутаться. «Что вы имеете в виду, что есть только один класс под названием« Вещи »?!?» - «Как мне продлить этот последний класс Thing!?!» - «Вы нарушаете принципы ООП !!!» - "Неправильно иметь все эти статические функции, которые действуют на любой объект!?!?"

Наконец, некоторые замечания по реализации:

  • Я использовал Java HashMap для свойств и «родительский» указатель для прототипа. Это работало нормально, но имело следующие недостатки: а) чтение свойств иногда приходилось отслеживать по длинной родительской цепочке, что ухудшало производительность. Б) если вы мутировали родительский прототип, изменение затронуло бы всех потомков, которые не переопределяли изменяемое свойство. Это может вызвать незначительные ошибки, если вы не будете осторожны!
  • Если бы я делал это снова, я бы использовал неизменяемые постоянные карты для свойств (вроде постоянных карт Clojure ) или мою собственную реализацию постоянных хеш-карт Java . Тогда вы получите преимущество дешевого копирования / изменений в сочетании с неизменным поведением, и вам не нужно будет постоянно связывать объекты с их родителями.
  • Вы можете повеселиться, если встраиваете функции / методы в свойства объекта. Взлом, который я использовал в Java для этого (анонимные подтипы класса «Script»), не был очень элегантным - если бы я делал это снова, я бы, вероятно, использовал подходящий легко встраиваемый язык для скриптов (Clojure или Groovy)


(+1) Это хороший анализ. Altought, он более основан на Java-модели, например, Delphi, C #, VB.Net имеет явные свойства.
umlcat

3
@umlcat - Я чувствую, что модель Java во многом совпадает с моделью Delphi / C # (кроме приятного синтаксического сахара для доступа к свойствам) - вам все равно нужно статически объявлять свойства, которые вы хотите в своем определении класса. Суть модели-прототипа в том, что это определение не является статичным, и вам не нужно заранее делать какие-либо заявления ....
mikera

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

Что касается упрощения множественного наследования, вы сравнили его с Java, который не поддерживает множественное наследование, но проще ли по сравнению с языками, которые его поддерживают, такими как C ++?
Пиовезан

2

Основное преимущество ООП на основе прототипов состоит в том, что объекты и «классы» могут быть расширены во время выполнения.

В ООП на основе классов есть несколько хороших функций, к сожалению, это зависит от языка программирования.

Object Pascal (Delphi), VB.Net & C # имеет очень прямой способ использовать свойства (не путать с полями) и методы доступа к свойствам, в то время как свойства Java и C ++ доступны через методы. И в PHP есть смесь обоих, называемых «магическими методами».

Существуют некоторые классы динамической типизации, хотя языки OO основного класса имеют статическую типизацию. Я думаю, что статическая типизация с Class OO очень полезна, потому что позволяет использовать функцию, называемую Object Introspection, которая позволяет создавать эти IDE и разрабатывать веб-страницы, визуально и быстро.


0

Я должен согласиться с @umlcat. Расширение класса является основным преимуществом. Например, предположим, что вы хотите добавить больше функциональности в строковый класс в течение длительного периода времени. В C ++ это можно сделать путем продолжения наследования предыдущих поколений строковых классов. Проблема этого подхода заключается в том, что каждое поколение по сути становится своим собственным типом, что может привести к массовому переписыванию существующих кодовых баз. С помощью прототипного наследования вы просто «присоединяете» новый метод к исходному базовому классу ... без повсеместного наращивания унаследованных классов и отношений наследования. Мне бы очень хотелось, чтобы C ++ придумал аналогичный механизм расширения в своем новом стандарте. Но их комитетом руководят люди, которые хотят добавить броские и популярные функции.


1
Возможно, вы захотите прочесть монолиты «Unstrung» , в которых std::stringуже слишком много членов, которые должны быть независимыми алгоритмами или, по крайней мере, не являющимися друзьями. И в любом случае, новые функции-члены могут быть добавлены без изменения макета в памяти, если вы можете изменить исходный класс.
Дедупликатор
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.