Введение

Процесс получения классов из существующих и организации их в иерархию.

В объектно-ориентированном программировании наследование — это механизм, основанный на создании объекта или класса на базе другого объекта (наследование на основе прототипа) или класса (наследование на основе класса) с сохранением схожей реализации. Также определяется как получение новых классов (подклассов) из существующих, таких как суперкласс или базовый класс, и последующее формирование из них иерархии классов. В большинстве объектно-ориентированных языков, основанных на классах, таких как C++, объект, созданный посредством наследования, – "дочерний объект" – приобретает все свойства и поведение "родительского объекта", за исключением: конструкторов, деструкторов, перегруженных операторов и дружественных функций базового класса. Наследование позволяет программистам создавать классы, построенные на основе существующих, определять новую реализацию при сохранении прежнего поведения (реализуя интерфейс), повторно использовать код и независимо расширять исходное программное обеспечение посредством публичных классов и интерфейсов. Отношения между объектами или классами, установленные посредством наследования, приводят к образованию ориентированного ациклического графа. Класс, полученный наследованием, называется подклассом своего родительского класса или суперкласса. Термин "наследование" используется в широком смысле как для программирования на основе классов, так и для программирования на основе прототипов, но в узком смысле он зарезервирован для программирования на основе классов (когда один класс наследует от другого), а соответствующая техника в программировании на основе прототипов называется делегированием (когда один объект делегирует другому). Паттерны наследования, модифицирующие классы, могут быть предопределены в соответствии с простыми параметрами сетевого интерфейса для обеспечения совместимости между языками. Наследование не следует путать с подтипированием. В некоторых языках наследование и подтипирование согласуются, в то время как в других они различаются; в общем случае, подтипирование устанавливает отношение "является", в то время как наследование лишь повторно использует реализацию и устанавливает синтаксическую связь, необязательно семантическую (наследование не гарантирует поведенческое подтипирование). Для разграничения этих понятий подтипирование иногда называют наследованием интерфейса (не учитывая, что специализация переменных типа также порождает отношение подтипирования), а наследование, как оно определено здесь, известно как наследование реализации или наследование кода. Тем не менее, наследование является распространенным механизмом для установления отношений подтипов. Наследование противопоставляется композиции объектов, когда один объект содержит другой объект (или объекты одного класса содержат объекты другого класса); см. композицию вместо наследования. Композиция реализует отношение "имеет", в отличие от отношения "является" в подтипировании.

История

В 1966 году Тони Хоар представил некоторые замечания о структурах данных, и в частности предложил идею подклассов структур, типов структур с общими свойствами, но различаемых по тегу варианта и имеющих поля, специфичные для данного варианта. Под влиянием этого, в 1967 году Оле Йохан Даль и Кристен Нюгард представили конструкцию, позволяющую определять объекты, принадлежащие к разным классам, но обладающие общими свойствами. Общие свойства объединялись в суперкласс, и каждый суперкласс сам потенциально мог иметь суперкласс. Таким образом, значения подкласса представляли собой составные объекты, состоящие из некоторого числа префиксных частей, принадлежащих различным суперклассам, плюс основной части, принадлежащей подклассу. Все эти части объединялись вместе. К атрибутам составного объекта можно было обращаться с использованием точечной нотации. Эта идея впервые была реализована в языке программирования Simula 67. Впоследствии она распространилась на Smalltalk, C++, Java, Python и многие другие языки.

Классы, не подлежащие подклассификации

В некоторых языках класс может быть объявлен как запрещающий наследование путем добавления определенных модификаторов класса к объявлению класса. Примеры включают ключевое слово `final` в Java и C++11 и более поздних версиях, или ключевое слово `sealed` в C#. Такие модификаторы добавляются в объявление класса перед ключевым словом `class` и объявлением идентификатора класса. Такие классы, запрещающие наследование, ограничивают возможность повторного использования, особенно когда разработчики имеют доступ только к предварительно скомпилированным двоичным файлам, а не к исходному коду. Класс, запрещающий наследование, не имеет подклассов, поэтому во время компиляции можно легко определить, что ссылки или указатели на объекты этого класса фактически ссылаются на экземпляры этого класса, а не на экземпляры подклассов (поскольку их не существует) или экземпляры суперклассов (приведение типа ссылки нарушает систему типов). Поскольку точный тип объекта, на который делается ссылка, известен до выполнения, можно использовать раннее связывание (также называемое статической диспетчеризацией) вместо позднего связывания (также называемого динамической диспетчеризацией), которое требует одного или нескольких поисков в виртуальной таблице методов в зависимости от того, поддерживает ли язык программирования множественное наследование или только одиночное наследование.

Непереопределяемые методы

Как классы могут быть не допускающими наследования, так и объявления методов могут содержать модификаторы, запрещающие переопределение метода (то есть замену его новой функцией с тем же именем и сигнатурой в подклассе). Приватный метод нельзя переопределить, поскольку он недоступен для классов, отличных от класса, частью которого он является (хотя это неверно для C++). Метод final в Java, sealed метод в C# или frozen feature в Eiffel нельзя переопределить.

Виртуальные методы

Если метод суперкласса является виртуальным, то вызовы этого метода будут выполняться с динамической диспетчеризацией. В некоторых языках программирования требуется явно объявлять метод как виртуальный (например, в C++), а в других все методы по умолчанию являются виртуальными (например, в Java). Вызов невиртуального метода всегда выполняется со статической диспетчеризацией (то есть адрес вызываемой функции определяется во время компиляции). Статическая диспетчеризация быстрее динамической и позволяет применять оптимизации, такие как встроенная подстановка.

Приложения

Наследование используется для установления связи между двумя или более классами.

Преобладает

Многие объектно-ориентированные языки программирования позволяют классу или объекту заменить реализацию аспекта — как правило, поведения — унаследованного от другого класса. Этот процесс называется переопределением. Переопределение вносит сложность: какую версию поведения будет использовать экземпляр унаследованного класса — ту, которая определена в самом классе, или ту, которая определена в родительском (базовом) классе? Ответ зависит от языка программирования, и некоторые языки предоставляют возможность указать, что конкретное поведение не должно быть переопределено и должно работать так, как определено в базовом классе. Например, в C# базовый метод или свойство может быть переопределено в подклассе только в том случае, если оно помечено модификаторами virtual, abstract или override, а в языках программирования, таких как Java, для переопределения других методов могут использоваться различные методы. Альтернативой переопределению является сокрытие унаследованного кода.

Наследственность против подтипизации

Наследование похоже на подтипирование, но отличается от него. (Сравните коннотацию и денотацию.) В некоторых объектно-ориентированных языках понятия повторного использования кода и подтипирования совпадают, поскольку единственный способ объявить подтип — определить новый класс, наследующий реализацию другого.

Вопросы и альтернативы

Наследование реализации является предметом споров среди программистов и теоретиков объектно-ориентированного программирования, по крайней мере, с 1990-х годов. Среди критиков – авторы шаблонов проектирования, которые выступают за наследование интерфейсов и предпочитают композицию наследованию. Например, шаблон «Декоратор» (как упоминалось выше) был предложен для преодоления статической природы наследования между классами. В качестве более фундаментального решения той же проблемы, ролевое программирование вводит отдельную связь – «играет роль», объединяя свойства наследования и композиции в новую концепцию. По мнению Аллена Холуба, основная проблема наследования реализации заключается в том, что оно создает ненужную связанность, проявляющуюся в виде «проблемы хрупкого базового класса»: изменения в реализации базового класса могут привести к непреднамеренным изменениям поведения в подклассах. Использование интерфейсов позволяет избежать этой проблемы, поскольку не происходит совместного использования реализации, а только API. Другими словами, «наследование нарушает инкапсуляцию». Эта проблема особенно заметна в открытых объектно-ориентированных системах, таких как фреймворки, где клиентский код должен наследовать классы, предоставляемые системой, и затем заменять классы системы в своих алгоритмах. По сообщениям, изобретатель Java, Джеймс Гослинг, критиковал наследование реализации, утверждая, что не стал бы включать его, если бы перепроектировал Java. Конструкции языков, отделяющие наследование от подтипирования (наследование интерфейсов), появились еще в 1990 году; современным примером является язык программирования Go. Сложное наследование или использование наследования в недостаточно зрелом дизайне может привести к проблеме «йо-йо». Когда в конце 1990-х годов наследование использовалось как основной подход к структурированию программ, разработчики, как правило, разбивали код на большее количество уровней наследования по мере расширения функциональности системы. Если команда разработчиков объединяла несколько уровней наследования с принципом единственной ответственности, это приводило к появлению множества очень тонких слоев кода, многие из которых состояли всего из 1–2 строк фактического кода. Слишком большое количество уровней затрудняет отладку, поскольку становится сложно определить, какой уровень необходимо отладить. Еще одна проблема наследования заключается в том, что подклассы должны быть определены в коде, что означает, что пользователи программы не могут добавлять новые подклассы во время выполнения. Другие шаблоны проектирования (например, «Сущность-компонент-система») позволяют пользователям программы определять варианты сущности во время выполнения.