Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Объектно-ориентированное программирование.PDF
Скачиваний:
208
Добавлен:
01.05.2014
Размер:
3.64 Mб
Скачать

converted to PDF by BoJIoc

необходимо только придать движение белому шару. Измените программу, чтобы использовать этот подход. Что дает более реальную модель? Почему?

Глава 7

Наследование

Первым шагом при изучении объектно-ориентированного программирования было осознание задачи как взаимодействия программных компонент. Этот организационный подход был главным при разборе примеров в главах 5 и 6.

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

7.1. Интуитивное описание наследования

Давайте вернемся к Фло хозяйке цветочного магазина из первой главы. Мы вправе ожидать от нее вполне определенного поведения не потому, что она хозяйка именно цветочного магазина, а потому, что она хозяйка магазина. Например, Фло попросит вас оплатить заказ, а затем даст вам квитанцию. Эти действия не являются уникальными для владельца цветочного магазина; они общие для булочников, бакалейщиков, продавцов канцелярских товаров и автомобилей и т. д. Таким образом, мы как бы связали определенное поведение с общей категорией «хозяева магазинов» Shopkeeper, и поскольку хозяева (и хозяйки) цветочных магазинов (Florist) являются частным случаем категории Shopkeeper, поведение для данного подкласса определяется автоматически.

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

Наследование всегда транзитивно, так что класс может наследовать черты надклассов, отстоящих от него на несколько уровней. Например, если собаки Dog являются подклассом класса млекопитающих Mammal, а млекопитающие Mammal являются подклассом класса животных Animal, то класс собак Dog наследует свойства и млекопитающих Mammal, и всех животных Animal.

Усложняющим обстоятельством в нашем интуитивном описании наследования является тот факт, что подклассы могут переопределять поведение, унаследованное от родительского класса. Например, класс утконосов Platypus переопределяет процедуру размножения, унаследованную от класса млекопитающих Mammal, поскольку утконосы откладывают яйца. В этой главе мы коротко коснемся механизма переопределения. К более детальному обсуждению семантики наследования мы вернемся в главе 11.

converted to PDF by BoJIoc

7.2. Подкласс, подтип и принцип подстановки

Рассмотрим связь между типом данных, связанным с родительским классом, и типом данных, связанным с дочерним классом. Можно утверждать следующее:

Представители подкласса должны владеть всеми областями данных родительского класса.

Представители подкласса должны обеспечивать выполнение, по крайней мере через наследование (если нет явного переопределения), всех функциональных обязанностей родительского класса. Это не противоречит тому, что у нового класса могут появиться дополнительные обязанности.

Представитель дочернего класса может имитировать поведение родительского класса и должен быть неотличим от представителя родительского класса в сходных ситуациях.

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

Принцип подстановки утверждает, что если есть два класса A и B такие, что класс B является подклассом класса A (возможно, отстоя в иерархии на несколько ступеней), то мы должны иметь возможность подставить представителя класса B вместо представителя класса A в любой ситуации, причем без видимого эффекта.

Как мы увидим в главе 10, термин подтип часто применяется к такой связи «классподкласс», для которой выполнен принцип подстановки, в отличие от общего случая, в котором этот принцип не всегда удовлетворяется.

Мы видели применение принципа подстановки в главе 6. В разделе 6.4 описывалась следующая процедура:

procedure drawBoard; var

gptr : GraphicalObject; begin

SetPort (theWindow);

(* нарисовать все графические объекты *) gptr := listOfObjects;

while gptr <> nil do begin

gprt.draw;

gptr := gptr.link; end;

end;

Глобальная переменная listOfObjects относится к списку графических объектов одного из трех типов. Переменная gptr объявлена просто как графический объект, хотя во время выполнения тела цикла она принимает значения, которые фактически представляют собой объекты порожденных подклассов. В одном случае gptr содержит шар, в другом лузу, а в третьем стенку. При обращении к функции draw всегда вызывается подходящий метод для текущего значения gptr, отличный от метода объявленного класса GraphicalObject. Для того чтобы эта процедура работала верно, необходимо, чтобы

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

converted to PDF by BoJIoc

7.2.1. Подтипы и строгий контроль типов данных

Языки программирования со статическими типами данных (такие, как C++ и Object Pascal) делают более сильный упор на принцип подстановки, чем это имеет место в языках с динамическими типами данных (Smalltalk и Objective-C). Причина этого в том,

что языки со статическими типами данных склонны характеризовать объекты через приписанные им классы, тогда как языки с динамическими типами данных через их поведение. Например, полиморфная функция (функция, которая может принимать в качестве аргументов объекты различных классов) в языке со статическими типами данных может обеспечить должный уровень функциональных возможностей, только потребовав, чтобы все аргументы были подклассами нужного класса. Поскольку в языке с динамическими типами данных аргументы вовсе не имеют типа, подобное требование просто означало бы, что аргумент должен уметь отвечать на определенный набор сообщений. Дальнейшее обсуждение статических и динамических типов данных мы проведем в главе 10, а детальное изучение полиморфизма в главе 14.

7.3. Формы наследования

Наследование применяется на удивление по-разному. В этом разделе мы опишем наиболее часто используемые варианты. Заметим, что в следующем списке приведены абстрактные категории общего характера, и он не претендует на полноту. Более того, иногда происходит так, что различные методы класса реализуют наследование по- разному.

В следующих разделах 1 обратите особое внимание на то, когда наследование обеспечивает порождение подтипов, а когда нет.

7.3.1. Порождение подклассов для специализации (порождение подтипов)

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

Вот пример порождения подклассов со специализацией. Класс окон Window предоставляет общие операции с окнами (сдвиг, изменение размеров, свертывание и т. д.). Специализированный подкласс текстовых окон TextEditWindow наследует операции с окнами и дополнительно обеспечивает средства, позволяющие окну отображать текстовую информацию, а пользователю ее редактировать. Поскольку класс TextEditWindow удовлетворяет всем свойствам, ожидаемым от окон в общем виде (и, следовательно, является подтипом класса Window в дополнение к тому, что он является его подклассом), мы распознаем такую ситуацию как порождение подклассов для специализации.

7.3.2. Порождение подкласса для спецификации

Следующий вариант использования наследования состоит в том, чтобы гарантировать поддержку классами определенного общего интерфейса, то есть реализацию ими одних и тех же методов. Родительский класс может быть комбинацией реализованных операций и операций, осуществление которых доверено дочерним классам. Часто нет никаких отличий в интерфейсе между родительским и дочерним классами последний просто обеспечивает выполнение описанного, но не реализованного в родительском классе поведения.

converted to PDF by BoJIoc

Фактически это специальный случай порождения специализирующего подкласса за исключением того, что подклассы являются не усовершенствованием

1 Описанные здесь категории взяты из [Halbert 1987], хотя я добавил несколько собственных. Пример «редактируемого окна» взят из [Meyer 1988a].

существующего типа, а, скорее, реализацией неполной, абстрактной спецификации. В

таких случаях родительский класс иногда называют абстрактно специфицированным классом.

В примере, имитирующем игру бильярд, в главе 6 класс графических объектов GraphicalObject является абстрактным классом, поскольку он не реализует методы отображения объектов и их реакцию на соприкосновение с шаром. Последующие классы Ball, Wall и Hole используют порождение подклассов для спецификации, обеспечивая реальное воплощение этих методов.

В общем случае порождение подклассов для спецификации распознается по тому, что фактическое поведение не определено в родительском классе оно только описано и будет реализовано в дочернем классе.

7.3.3. Порождение подкласса с целью конструирования

Часто класс наследует почти все функциональное поведение родительского класса, изменяя только имена методов или определенным образом модифицируя аргументы. Это может происходить даже в том случае, когда новому и родительскому классам не удается сохранить между собой отношение «быть экземпляром» (is-a relationship).

Например, иерархическая структура классов в языке Smalltalk реализует обобщение массива, называемое Dictionary (словарь). Словарь представляет собой набор пар «ключзначение»; ключи могут быть произвольными. Таблицу символов, которую можно было бы использовать в компиляторе, разумно представить словарем, в котором индексами служат символические имена, а значения имеют фиксированный формат, определенный для отдельных записей этой таблицы. Следовательно, класс SymbolTable (таблица символов) должен быть порожден из класса Dictionary введением новых методов, специфичных для таблицы символов. Другим примером может служить построение совокупности абстрактных данных на основе базового класса, обеспечивающего методы работы со списками. В обоих случаях дочерний класс не является более специализированной формой родительского класса, так как у нас и в мыслях не будет подставлять представителей дочернего класса туда, где используются представители родительского класса.

Типичный пример порождения с целью конструирования наблюдается при создании класса, записывающего значения в двоичный файл, например, в системах хранения информации. Родительский класс, как правило, обеспечивает запись только неструктурированных двоичных данных. Подкласс строится для каждой структуры, требующей сохранения. Он реализует процедуру хранения для определенного типа данных, которая использует методы родительского класса для непосредственной записи 1

.

1 Этот пример иллюстрирует расплывчатость категорий. Если дочерний класс реализует процедуру хранения, используя другое имя метода, мы говорим, что это наследование для конструирования. Если же дочерний класс пользуется тем же именем, что и родительский класс, мы могли бы сказать, что это наследование для спецификации.

class Storable

{

converted to PDF by BoJIoc

void writeByte(unsigned char);

}

class StoreMyStruct : public Storable

{

void writeStruct (MyStruct &aStruct);

}

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

Мы изучим конкретный вариант порождения с целью конструирования в главе 8. Там же мы узнаем, что язык программирования C++ предоставляет интересный механизм: закрытое наследование, который позволяет порождать подклассы для конструирования без нарушения принципа подстановки.

7.3.4. Порождение подкласса для обобщения

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

Рассмотрим систему графического отображения, в которой был определен класс окон Window для черно-белого фона. Мы хотим создать тип цветных графических окон ColoredWindow. Цвет фона будет отличаться от белого за счет добавления нового поля, содержащего цвет. Придется также переопределить наследуемую процедуру изображения окна, в которой происходит фоновая заливка.

Порождение подкласса для обобщения часто используется в случае, когда общий проект основывается в первую очередь на значениях данных и меньше на функциональном поведении. Это видно на примере с цветным окном, так как оно содержит поля данных, необязательные в случае черно-белого окна.

Как правило, следует избегать порождения подкласса для обобщения, пользуясь перевернутой иерархией типов и порождением для специализации. Однако это не всегда возможно.

7.3.5. Порождение подкласса для расширения

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

Примером порождения подкласса для расширения является множество текстовых строк StringSet, наследующее свойства общего класса множеств Set и предназначенное для хранения строковых величин. Такой класс мог бы предоставлять дополнительные методы для строковых операций, например «найти по префиксу», который возвращал бы подмножество всех элементов множества, начинающихся с определенной подстроки. Такие операции имеют смысл для подкласса, но не для родительского класса.

converted to PDF by BoJIoc

Поскольку функциональные возможности родителя остаются нетронутыми и доступными, порождение подкласса для расширения не противоречит принципу подстановки и, следовательно, такие подклассы всегда будут подтипами.

7.3.6. Порождение подкласса для ограничения

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

Допустим, существующая библиотека классов предоставляет структуру данных Deque (double-ended-queue, очередь с двумя концами). Элементы могут добавляться или удаляться с любого конца структуры типа Deque, но программист желает создать класс Stack, вводя требование добавления или удаления элементов только с одного конца стека.

Таким же образом, как и при порождении для конструирования, программист может построить класс Stack как подкласс класса Deque и модифицировать нежелательные методы так, чтобы они выдавали сообщение об ошибке в случае применения. Такие методы переопределяют существующие и ограничивают их возможности, что характеризует порождение подкласса для ограничения. (Переопределение, с помощью которого подкласс изменяет смысл метода родительского класса, обсуждается в следующей главе.)

Поскольку порождение подкласса для ограничения является явным нарушением принципа подстановки и поскольку оно строит подклассы, не являющиеся подтипами, его следует избегать, где только возможно.

7.3.7. Порождение подкласса для варьирования

Порождение подкласса для варьирования применяется, когда два класса имеют сходную реализацию, но не имеют никакой видимой иерархической связи между абстрактными понятиями, ими представляемыми. Например, программный код для управления мышкой может быть почти идентичным тому, что требуется для управления графическим планшетом. Теоретически, однако, нет никаких причин, для того чтобы класс Mouse, управляющий манипулятором «мышь», был подклассом класса Tablet, контролирующего графический планшет, или наоборот. В таком случае в качестве родителя произвольно выбирается один из них, при этом другой наследует общую программную часть кода и переопределяет код, зависящий от устройства.

Обычно, однако, лучшей альтернативой является выделение общего кода в абстрактный класс, скажем PointingDevice, и порождение обоих классов от этого общего предка. Как и в случае порождения подкласса для обобщения, такой путь может быть недоступен при доработке уже существующего класса.

7.3.8. Порождение подкласса для комбинирования

Обычным является желание иметь подкласс, представляющий собой комбинацию двух или более родительских классов. Ассистент учителя, например, имеет характерные особенности как учителя, так и учащегося и, следовательно, может вести себя двояко.

Способность наследовать от двух или более родительских классов известна как множественное наследование; оно достаточно сложно и коварно, и мы посвятим этому вопросу целую главу.

converted to PDF by BoJIoc

7.3.9. Краткое перечисление форм наследования

Мы можем подвести итог изучению различных форм наследования в виде следующей таблицы:

Специализация. Дочерний класс является более конкретным, частным или специализированным случаем родительского класса. Другими словами, дочерний класс является подтипом родительского класса.

Спецификация. Родительский класс описывает поведение, которое реализуется в дочернем классе, но оставлено нереализованным в родительском.

Конструирование. Дочерний класс использует методы, предоставляемые родительским классом, но не является подтипом родительского класса (реализация методов нарушает принцип подстановки).

Обобщение. Дочерний класс модифицирует или переопределяет некоторые методы родительского класса с целью получения объекта более общей категории.

Расширение. Дочерний класс добавляет новые функциональные возможности к родительскому классу, но не меняет наследуемое поведение.

Ограничение. Дочерний класс ограничивает использование некоторых методов родительского класса.

Варьирование. Дочерний и родительский классы являются вариациями на одну тему, и связь «классподкласс» произвольна.

Комбинирование. Дочерний класс наследует черты более чем одного родительского класса. Это множественное наследование; оно будет рассмотрено в одной из следующих глав.

7.4. Наследование в различных языках программирования

В следующих разделах мы подробно опишем синтаксис, используемый для описания наследования в каждом из рассматриваемых языков программирования. Обратите внимание на разницу между теми языками, которые требуют, чтобы все классы были порождениями общего родительского класса (обычно называемого Object, как в языках Smalltalk и Objective-C), и теми, которые допускают различные независимые иерархии классов.

Преимущество наследования с единым предком в том, что функциональные возможности последнего (класса Object) наследуются всеми объектами. Таким образом гарантируется, что каждый объект обладает общим минимальным уровнем функциональности. Минус в том, что единая иерархия «зацепляет» все классы друг с другом.

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

Различные взгляды на объекты отчасти являются еще одним различием между языками с динамическими и статическими типами данных (мы вернемся к этой проблеме в главе 12).

В языках программирования с динамическими типами данных объекты в основном характеризуются теми сообщениями, которые они понимают. Если два объекта понимают одно и то же множество сообщений и реагируют на них сходным образом, они с практической точки зрения неразличимы, невзирая на их родственные связи. В таких

converted to PDF by BoJIoc

случаях разумно, чтобы все объекты наследовали большую часть своего поведения от общего базового класса.

7.4.1. Наследование в языке Object Pascal

В языке программирования Object Pascal фирмы Apple наследование от родительского

класса указывается помещением его имени в круглые скобки после ключевого слова object. Предположим, например, что в нашей имитации бильярда мы решили породить классы Ball, Wall и Hole от общего класса GraphicalObject. Мы сделаем это так, как показано в листинге 7.1.

Листинг 7.1. Пример наследования в языке Object Pascal фирмы Apple

type

GraphicalObject = object (* поля данных *) region : Rect;

link : GraphicalObject; (* операции *) procedure draw; procedure update;

procedure hitBy(aBall : Ball); end;

Ball = object(GraphicalObject) (* поля данных *)

direction : real; energy : real;

(* инициализация *)

procedure initialize (x, y : integer); (* переопределяемые методы *) procedure draw; override;

procedure update; override;

procedure hitBy(aBall : Ball); override; (* методы, специфичные для класса *) procedure erase;

procedure setCenter(newx, newy : integer); function x : integer;

function y : integer; end;

Как показано в этом примере, дочерние классы могут добавлять как новые поля данных, так и новые методы. Кроме того, они модифицируют существующее поведение, переопределяя методы с помощью ключевого слова override, как сделано для метода draw. Аргументы в переопределенном методе должны совпадать по типу и числу с аргументами метода родительского класса.

Версия языка Object Pascal фирмы Borland (система Delphi) отличается в двух важных отношениях. Во-первых, как мы видели в предыдущих главах, вместо ключевого слова object используется слово class, и классы всегда должны выводиться один из другого. Класс TObject — общий предок всех объектов. Во-вторых, в дополнение к ключевому слову override директива virtual помещается после описания тех методов родительского класса, которые могут быть переопределены почти как в языке C++. Пример, иллюстрирующий эти изменения, приведен в листинге 7.2. Пропуск ключевого слова override является частым источником ошибок, поскольку описание остается синтаксически законным, а его интерпретация неверной. Это мы обсудим подробнее в главе 10.

converted to PDF by BoJIoc

Листинг 7.2. Пример наследования в языке Delphi Pascal

type GraphicalObject = class(TObject)

(* поля данных *) region : Rect;

link : GraphicalObject; (* операции *) procedure draw; virtual;

procedure update; virtual;

procedure hitBy(aBall : Ball); virtual; end;

Ball = class(GraphicalObject) (* поля данных *) direction : real;

energy : real;

(* инициализация *)

procedure initialize (x, y : integer); (* переопределяемые методы *) procedure draw; override;

procedure update; override;

procedure hitBy(aBall : Ball); override; (* методы, специфичные для класса *) procedure erase;

procedure setCenter(newx, newy : integer); function x : integer;

function y : integer; end;

Более существенным различием между языками программирования Delphi Pascal и Apple Object Pascal является введение динамических методов. Они используют другие механизмы поиска во время выполнения программы (больше напоминающие Objective-C, чем C++; см. главу 21). Это делает динамические методы более медленными, чем виртуальные, но они требуют меньше памяти. Ключевое слово dynamic вместо virtual показывает, что объявляется динамический метод. Многие методы, связанные с действиями операционной системы по управлению окнами, реализованы как динамические. Значение термина сообщение часто ограничивается только действиями, связанными с управлением окнами.

7.4.2. Наследование в языке Smalltalk

Как отмечено нами в главе 3, наследование как средство создания новых классов обязательно в языке Smalltalk. Новый класс не может быть определен без предварительного описания существующего класса, которому он наследует. Фактически новый класс создается с помощью сообщения родительскому классу.

List subclass: #Set instanceVariables: #() classVariables: #()

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

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

converted to PDF by BoJIoc

7.4.3. Наследование в языке Objective-C

Как и в языке Smalltalk, для Objective-C наследование является неотъемлемой частью формирования нового класса. Описание интерфейса каждого нового класса должно определить предка, от которого происходит наследование. Следующий пример показывает, что класс игральных карт Card происходит от универсального класса Object:

@interface Card : Object

{

. . .

}

. . .

@end

Как и в языке Smalltalk, существует единый предок Object, от которого в конечном счете происходят все остальные классы. Object обеспечивает все объекты общей и полной функциональностью. Он часто используется как родитель для нового класса.

Разрешается только одиночное наследование, то есть класс не может наследовать от двух или более родителей. Как и в языке Smalltalk, метод, имеющий то же имя, что и метод родительского класса, переопределяет его.

7.4.4. Наследование в языке C++

В отличие от Smalltalk и Objective-C новый класс в языке программирования C++ не обязан происходить от уже существующего класса. Наследование указывается в заголовке описания класса с помощью ключевого слова public, за которым следует имя родительского класса. Новый класс TablePile происходит от более общего класса CardPile, представляющего собой колоду карт:

class TablePile : public CardPile

{

. . .

};

Ключевое слово public может быть заменено на слово private, указывающее на порождение класса для конструирования, то есть на форму наследования, не создающую подтипа. Такой пример будет рассмотрен в главе 11.

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

обеспечения наследования конструктор дочернего класса должен явно вызвать конструктора родительского класса. Делается это с помощью инициализирующего предложения в конструкторе дочернего класса:

TablePile::TablePile (int x, int y, int c) : CardPile(x,y) // инициализация родителя

{

// теперь инициализируем дочерний класс

. . .

}

Язык программирования C++ поддерживает множественное наследование, то есть новый класс может быть определен как потомок двух или более родителей. Мы исследуем смысл этого более подробно в следующей главе.

В предыдущей главе мы описали ключевые слова public (открытый) и private (закрытый), указав, что одно описывает интерфейс класса, а другое детали реализации. В описании классов, получаемых наследованием, может быть использовано третье ключевое слово

converted to PDF by BoJIoc

protected (защищенный). Защищенные поля являются частью реализации, но доступны подклассам так же, как и самому классу.

Когда ключевое слово virtual предшествует описанию входящей в класс функции, оно означает, что эта функция, вероятно, будет переопределена в подклассе или что эта функция сама переопределяет функцию надкласса. (Это ключевое слово является необязательным в дочернем классе, но его желательно оставлять.) Однако семантика переопределения в языке C++ является тонким моментом и зависит от того, как был описан получатель, которому присваивается объект. Мы отложим ее обсуждение до главы 11.

7.4.5. Наследование в языке Java

Из всех обсуждаемых в этой книге языков Java идет дальше всех в разделении понятий подкласса и подтипа. Подклассы объявляются с помощью ключевого слова extends, как в следующем примере:

class window

{

// . . .

}

class textEditWindow extends window

{

// . . .

}

Предполагается, что подклассы являются подтипами (хотя, как и в случае языка C++, это предположение не всегда верно). Это означает, что представитель подкласса может быть присвоен переменной, объявленной с типом родительского класса. Методы дочернего класса, имеющие те же имена, что и методы родителя, переопределяют наследуемое поведение. Как и в языке C++, ключевое слово protected может быть использовано для указания методов и данных, доступных только внутри класса или подкласса, но не входящих в более общий интерфейс.

Все классы происходят от единого предка Object. Если родительский класс не указан явно, то предполагается класс Object. Таким образом, определение класса window, приведенное выше, эквивалентно следующей записи:

class window extends Object

{

// . . .

}

Альтернативная форма порождения подтипов данных основана на интерфейсе. Интерфейс определяет протокол определенного поведения, а не реализацию. В этом отношении он подобен абстрактному родительскому классу. В следующем примере приведен интерфейс, описывающий объекты, которые могут читать и писать во входной/выходной поток.

public interface Storing

{void writeOut(Stream s); void readFrom(Stream s);

}

Интерфейс определяет новый тип. Это означает, что переменные могут быть объявлены просто с именем интерфейса. А класс может указать, что он реализует протокол, определенный интерфейсом. Представители класса могут присваиваться переменным, объявленным с типом интерфейса, точно так же, как представители дочернего класса могут присваиваться переменным, объявленным с типом родительского класса:

public class BitImage implements Storing

{void writeOut (Stream s)

{

converted to PDF by BoJIoc

// . . .

};

void readFrom (Stream s)

{

// . . .

};

};

Хотя язык Java поддерживает только одиночное наследование (наследование исключительно от одного родительского класса), класс может указывать, что он поддерживает несколько интерфейсов (реализует множественный интерфейc). Многие проблемы, для которых в языке C++ пришлось бы использовать множественное наследование, в языке программирования Java разрешаются через множественные интерфейсы. Интерфейсам позволено расширять другие интерфейсы, в том числе и множественные, через указание ключевого слова extend.

В языке Java идея порождения подкласса для спецификации формализована через модификатор abstract. Если класс объявлен как abstract, то из него должны порождаться подклассы. Не разрешается создавать представителей абстрактного класса, можно только порождать подклассы. Методы тоже могут быть объявлены как abstract, и в таком случае они не обязаны иметь реализацию. Таким образом, объявление класса как abstract обеспечивает, что он будет использоваться только как спецификация поведения, а не в виде конкретных объектов:

abstract class storable

{

public abstract writeOut();

}

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

final class newClass extends oldClass

{

. . .

}

7.5. Преимущества наследования

В этом разделе мы опишем некоторые из многих важных преимуществ правильного использования механизма наследования.

7.5.1. Повторное использование программ

При наследовании поведения от другого класса программный код не нуждается в переписывании. Это кажется очевидным, но на самом деле имеет важное значение. Многие программисты тратят массу времени на переработку кода, уже написанного не один раз до этого, — например, при поиске по образцу в строке или при включении нового элемента в таблицу. С введением объектно-ориентированной техники, эти функции могут быть написаны однажды и потом повторно использоваться.

Другое преимущество повторно используемого кода в его повышенной надежности (чем в большем числе ситуаций используется код, тем больше возможностей обнаружения ошибок) и низкой стоимости, так как она делится на всех пользователей кода.

converted to PDF by BoJIoc

7.5.2. Использование общего кода

При применении объектно-ориентированной техники использование общего кода происходит на нескольких уровнях. Во-первых, клиенты могут пользоваться одними и теми же классами (Бред Кокс [Cox 1986] называет их software-IC, то есть программными интегральными схемами, по аналогии с аппаратными интегральными схемами). Иная форма использования общего кода возникает в случае, когда два или более класса, разработанных тем же самым программистом для некоторого проекта, наследуют от единого родительского класса. Например, множество Set и массив Array могут рассматриваться как разновидности совокупности данных Collection. В этом случае два или более типов объектов совместно используют наследуемый код. Он пишется единожды и входит в программу только в одном месте.

7.5.3. Согласование интерфейса

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

7.5.4. Программные компоненты

В главе 1 мы отмечали, что наследование предоставляет программистам возможность создавать повторно (многократно) используемые программные компоненты. Цель: обеспечить развитие новых приложений с минимальным написанием нового кода. Уже сейчас доступны несколько коммерческих библиотек такого типа, и в будущем мы можем ожидать появления многих новых специализированных систем.

7.5.5. Быстрое макетирование

Когда программное обеспечение конструируется в основном из повторно используемых компонент, большая часть времени, требуемого на разработку, может быть посвящена пониманию новых необычных частей системы. Таким образом программные комплексы могут создаваться быстрее и проще, приводя к стилю программирования, известному как быстрое макетирование или исследовательское программирование. Создается система- прототип (макет), пользователи экспериментируют с ней, потом на основе этих опытов создается вторая система, с ней проводятся эксперименты и т. д. Такое программирование особенно выгодно в ситуации, когда цели и требования к системе в начале разработки представлены весьма расплывчато.

7.5.6. Полиморфизм и структура

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

Обычно мобильность кода уменьшается с увеличением абстракции, то есть программы нижнего уровня могут быть использованы в нескольких различных проектах, и даже, возможно, абстракции следующего уровня могут повторно использоваться, но программы верхнего уровня тесно связаны с определенным приложением. Компоненты нижнего уровня могут быть перенесены в новую систему, и часто есть смысл в их самостоятельном существовании. Компоненты верхнего уровня обычно имеют смысл (из-за их функциональности или зависимости от данных), только когда они построены на определенных элементах нижнего уровня.