Тогда класс прямоугольника Rectangle можно унаследовать от Polygon. При этом Rectangle будет иметь все атрибуты и методы класса Polygon. Так, может уже быть написан метод, вычисляющий периметр путем суммирования длин всех сторон. Если все было реализовано правильно, этот метод автоматически будет работать и для нового класса; переписывать код не придется.

Если класс B наследует классу A, мы говорим, что B является подклассом A, а A — суперкласс B. По-другому говорят, что А — базовый или родительский класс, а B — производный или дочерний класс.

Как мы видели, производный класс может трактовать методы своего базового класса как свои собственные. С другой стороны, он может переопределить метод базового класса, предоставив иную его реализацию. Кроме того, в большинстве языков есть возможность вызвать из переопределенного метода метод базового класса с тем же именем. Иными словами, метод fоо класса B знает, как вызвать метод foo класса A. (Любой язык, не предоставляющий такого механизма, можно заподозрить в отсутствии истинной объектной ориентированности.) То же верно и в отношении атрибутов.

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

Это подводит нас к теме множественного наследования. Можно представить себе класс, который наследует нескольким классам. Например, классы Dog (Собака) и Cat (Кошка) могут наследовать классу Mammal (Млекопитающее), а Sparrow (Воробей) и Raven (Ворон) — классу WingedCreature (Крылатое). Но как быть с классом Bat (ЛетучаяМышь)? Он с равным успехом может наследовать и Mammal, и WingedCreature! Это хорошо согласуется с нашим жизненным опытом, ведь многие вещи можно отнести не к одной категории, а сразу к нескольким, не вложенным друг в друга.

Множественное наследование, вероятно, наиболее противоречивая часть ООП. Некоторые указывают на потенциальные неоднозначности, требующие разрешения. Например, если в обоих классах Mammal и WingedCreature имеется атрибут size (размер) или метод eat (есть), то какой из них имеется в виду, когда мы обращаемся к нему из объекта класса Bat? С этой трудностью тесно связана проблема ромбовидного наследования; она называется так из-за формы диаграммы наследования, возникающей, когда оба суперкласса наследуют одному классу. Представьте себе, что классы Mammal и WingedCreature наследуют общему предку Organism (Организм); тогда иерархия наследования от Organism к Bat будет иметь форму ромба. Но как быть с атрибутами, которые оба промежуточных класса наследуют от своего родителя? Получает ли Bat две копии? Или они должны быть объединены в один атрибут, поскольку все равно заимствованы у общего предка?

Это скорее проблемы проектировщика языка, а не программиста. В разных объектно- ориентированных языках они решаются по-разному. Иногда вводятся правила, согласно которым какое-то одно определение атрибута «выигрывает». Либо же предоставляется возможность различать одноименные атрибуты. Иногда даже язык позволяет вводить псевдонимы или переименовывать идентификаторы. Многими это рассматривается как аргумент против множественного наследования — о механизмах разрешения подобных конфликтов имен нет единого мнения, поэтому все они «языкозависимы». В языке C++ предлагается минимальный набор средств для разрешения неоднозначностей; механизмы языка Eiffel, наверное, получше, а в Perl проблема решается совсем по-другому.

Есть и альтернатива — полностью запретить множественное наследование. Такой подход принят в языках Java и Ruby. На первый взгляд, это даже не назовешь компромиссным решением, но, вскоре мы убедимся, что все не так плохо, как кажется. Мы познакомимся с приемлемой альтернативой традиционному множественному наследованию, но сначала обсудим полиморфизм — еще одно понятие из арсенала ООП.

1.1.3. Полиморфизм

Термин «полиморфизм», наверное, вызывает самые жаркие семантические споры. Каждый знает, что это такое, но все понимают его по-разному. (Не так давно вопрос «Что такое полиморфизм?» стал популярным во время собеседования при поступлении на работу. Если его зададут вам, рекомендую процитировать какого-нибудь эксперта, например Бертрана Мейера или Бьерна Страуструпа; если собеседник не согласится, то пусть он спорит с классиком, а не с вами.)

Буквально слово «полиморфизм» означает «способность принимать разные формы или обличья». В самом широком смысле так называют ситуацию, когда различные объекты по-разному отвечают на одно и то же сообщение или вызов метода.

Дамиан Конвей (Damian Conway) в книге «Object-Oriented Perl» проводит смысловое различие между двумя видами полиморфизма. Первый, наследственный полиморфизм, - то, что имеет в виду большинство программистов, говорящих о полиморфизме.

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

В языках со статической типизацией, например в C++, наследственный полиморфизм гарантирует совместимость типов вниз по цепочке наследования (но не в обратном направлении). Скажем, если B наследует A, то указатель на объект класса А может указывать и на объект класса в; обратное же неверно. Совместимость типов — существенная черта ООП в подобных языках, можно даже сказать, что полиморфизм ей и исчерпывается. Но, конечно же, полиморфизм можно реализовать и в отсутствие статической типизации (как в Ruby).

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

Читатели, знакомые с языком Java, понимают, что в нем реализованы оба вида полиморфизма. Класс в Java может расширять другой класс, наследуя ему с помощью ключевого слова extends, а может с помощью ключевого слова implements реализовывать интерфейс, за счет чего приобретает заранее известный набор методов (которые необходимо переопределить). Такой синтаксис позволяет интерпретатору Java во время компиляции определить, можно ли вызывать данный метод для конкретного объекта.

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

Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

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

Отметить Добавить цитату