Чем прототипное наследование отличается от классического?
Этот пост посвящен простому диалогу, который можно часто услышать к примеру на интервью на должность веб-разработчика:
ИНТЕРВЬЮЕР: Какой тип наследования в JavaScript?
КАНДИДАТ: Очевидно, что в JavaScript прототипное наследование.
ИНТЕРВЬЮЕР: Хорошо, а чем прототипное наследование отличается от классического наследования ООП?
А вот дальше кандидату следует уточнить что имеется ввиду. Если подразумевается что от него ожидается, что он начнет рассказывать о том как устроено прототипное наследование в JavaScript то это один момент, а если от него ожидает рассказ, о том чем парадигма прототипного наследования отличается от классического то это совсем другое.
В первом случае все достаточно просто, и как правило все кто хоть немного знает JavaScript готовы быстро ответить на этот вопрос (ну или произнести нужные ключевые слова типа атрибут __proto__, метод prototype и так далее).
А вот во втором случае все сложнее. Можно проработать 100 лет веб программистом, но толком не обратить внимание на этот вопрос. На практике с ним редко когда столкнешься. Но тем не менее, найти ответ на него было очень интересно.
Итак я попробую в этом посте освятить разницу между парадигмами прототипного и классического наследования. Прошу не судить меня слишком строго, если вы будете не согласны с моим мнением прошу в комментарии.
Как прототипное наследование, так и классическое наследование являются парадигмами объектно-ориентированного программирования (т. е. они имеют дело с объектами). Объекты — это просто абстракции, которые состоят из свойств сущности из реального мира (т. е. они представляют в программе сущности из реального мира в виде слов ). И это называется абстракция.
То есть абстракция — это представление вещей реального мира в компьютерных программах.
Теоретически абстракция определяется как «общая концепция, сформированная путем извлечения общих черт из конкретных примеров». Именно ради этого объяснения мы будем использовать вышеупомянутое определение.
Некоторые объекты имеют много общего. Например, ботинок имеет гораздо много общего с туфлей, и нечего с деревом. И поэтому ботинок и туфлю можно обобщить и назвать обувь. Так или иначе, не задумываясь об это, мы постоянно создаем эти ментальные организации.
Туфля и Ботинок — это Обувь. Следовательно, обувь — это обобщение как туфли, так и ботинка.
обувь | +---------------------------------+ | | v v туфля ботинок
В приведенном выше примере обувь, туфля и ботинок — все это абстракции. Однако обувь является более общей абстракцией ботинка и туфли.
Обобщение так же может быть абстракция другой более конкретной абстракции.
Так как в объектно-ориентированном программировании мы постоянно должны создавать абстракции то для этого были придуманы два инструмента объекты и классы, а так же наследование. Через наследования мы создаем обобщения. Обувь — это обобщение ботинка. Следовательно ботинок должен наследовать от обуви.
Цель объектно-ориентированного программирования — максимально точно имитировать эти категории реального мира. Давайте возьмем наш пример обуви и пойдем дальше.
Парадигма классического наследования
В классическом наследовании объекты являются абстракциями «вещей» реального мира, но мы можем ссылаться на объекты только через классы. Классы — в данном случае это обобщение объекта. Другими словами, получается что классы — это абстракция объекта реального мира. При обобщении мы наследуем один класс от другого. И при классическом наследовании процесс наследования должен создавать уровень абстракции. При каждом наследовании, каждый дочерний класс должен повышать уровень абстракции, тем самым повышая уровень обобщения. Вот пример классического наследования:
class Shoe { // ... } class Boot extends Shoe { // ... } Man hikingBoot = new Boot();
Как вы можете видеть в классических объектно-ориентированных языках программирования классы являются обобщениями и при каждом наследовании у них должен снижаться уровень абстракции.
Объекты в классических объектно-ориентированных языках программирования могут быть созданы только путем создания экземпляров классов.
Следовательно, по мере увеличения уровня абстракции сущности становятся более общими, а по мере снижения уровня абстракции сущности становятся более конкретными. В этом смысле уровень абстракции аналогичен шкале, варьирующейся от более специфических сущностей до более общих сущностей.
Задача программиста при использовании парадигмы классического наследования создать иерархию сущностей от максимальной общей к максимально конкретной.
Парадигма прототипного наследования
В отличие от классического наследования, прототипное наследование не имеет дело с увеличивающимися уровнями абстракции. Объект — это либо абстракция реальной вещи, как и раньше, либо прямая копия другого Объекта (другими словами, Прототипа (Prototype)). Объекты могут быть созданы из ничего, или они могут быть созданы из других объектов.
Если взять наш прежний пример то вряд ли нам удастся избежать иерархии абстракций. Поэтому он будет выглядеть примерно так:
var shoe = {}; var boot = Object.create(shoe); var hikingBoot = Object.create(boot);
Но в нем есть главное отличие. shoe, boot и hikingBoot это все независимые объекты. Просто одни объекты созданы от других. Это важно! А при классическом наследование обобщения являются абстракциями абстракций… от абстракций … вплоть до самого последнего потомка.
Было бы правильнее в данном случае привести пример из независимых объектов, но для объяснения разницы думаю этого будет достаточно.
Уровень абстракции здесь не обязан быть глубже одного уровня (хотя при желание может и быть).
При использование парадигмы прототипного наследования программист имеет дело только с объектами и при этом у него есть возможность создавать сущности в одном уровне абстракции.
Вы можете комбинировать обе формы наследования для достижения очень гибкой системы повторного использования кода. Что собственно почти всегда и происходит в реальном коде JavaScript. То есть в реальных проектах обычно подсознательно реализуется классическое наследование, через иерархию объектов, хотя это делать не обязательно. Так как реализовать классическое наследование с помощью прототипов очень легко. И да, обратное утверждение будет неверным.
Прототипное наследование позволяет реализовать большинство важных функций, которые вы найдете в классических языках ООП. В JavaScript замыкания и фабричные функции позволяют реализовать приватное состояние, а функциональное наследование можно легко комбинировать с прототипами, что также позволяет использовать миксины.
Некоторые преимущества прототипного наследования:
Слабая связь. Экземпляр никогда не нуждается в прямой ссылке на родительский класс или прототип. Можно сохранить ссылку на прототип объекта, но это не рекомендуется, потому что это будет способствовать тесной связи в иерархии объектов — одна из самых больших ошибок классического наследования.
Плоские иерархии. С прототипным наследованием легко поддерживать плоские иерархии наследования — используя конкатенацию (выборочное использование свойств одного объекта для создания другого ) и делегирование (клонирование одного объекта в другой), вы можете иметь один уровень делегирования объекта и один экземпляр без ссылок на родительские классы.
Тривиальное множественное наследование. Наследование от нескольких предков так же просто, как объединение свойств из нескольких прототипов с использованием конкатенации для формирования нового объекта или нового делегата для нового объекта.
Гибкая архитектура. Поскольку вы можете выборочно наследоваться, вам не нужно беспокоиться о проблеме «неправильного дизайна». Новый класс может наследовать любую комбинацию свойств от любой комбинации исходных объектов. Из-за простоты выравнивания иерархии, изменение в одном месте не обязательно вызывает рябь в длинной цепочке объектов-потомков.
Нет больше горилл с банами и джунглями. Селективное наследование устраняет эту проблему как и проблему ромба.
Заключение
В заключении отвечая на вопрос чем отличается классическое наследование от прототипного, можно сказать, что при следовании парадигме классического наследования нам необходимо создавать иерархию классов от общему к частному создавая тем самым при каждом наследовании дополнительный уровень абстракции. При следовании парадигме прототипного наследования мы не обязаны создавать иерархию от общего к частному, мы можем это делать а можем и не делать. Это оставляет нам свободу выбора (независимо от того понимаем мы это или нет), что и является на мой взгляд главным отличием этих двух парадигм.
Если у вас есть свое мнение на этот вопрос, добро пожаловать в комментарии.
Источники используемые при создание данного поста:
crishanks — Classical vs. Prototypal Inheritance
Aadit M Shah — classical inheritance vs prototypal inheritance in javascript
Aadit M Shah — Why Prototypal Inheritance Matters
Eric Elliott — Classical Vs prototypal inheritance
Опечатка. Уровень абстракции снижается (каждый класс становится более конкретным) .
«Как вы можете видеть в классических объектно-ориентированных языках программирования классы являются обобщениями и при каждом наследования у них должен расти уровень абстракции.»
Спасибо.
тсити
Получается вертикальная иерархия и горизонтальная иерархия
Очень тяжело читать… сумбурно написано
Не даны определения понятий абстракции и обобщения. ТО что есть это не определения понятий, а отсебятина.
По-моему, все понятно написано. Такого рода теорию всегда трудно объяснять простым языком. Спасибо)