М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 46
Текст из файла (страница 46)
Множественное наследование
Язык Eiffel поддерживает множественное наследование:
class Winged_Vehicle
feature
Weight: Integer;
display is . .. end;
end;
class Motorized_Vehicle
feature
Weight: Integer;
display is ... end;
end;
class Airplane inherit
Winged_Vehicle, Motorized_Vehicle
…
end;
Поскольку допускается множественное наследование, в языке должно определяться, как разрешить неоднозначности, если имя унаследовано от нескольких предков. Правило языка Eiffel в основе своей очень простое (хотя его формальное определение сложное, поскольку оно должно принимать во внимание все возможности иерархии наследования):
Если свойство унаследовано от класса предка более чем одним путем, оно используется совместно; в противном случае свойства реплицируются.
rename- и redef ine-конструкции могут использоваться для изменения имен по мере необходимости. В примере класс Airplane наследует только одно поле Weight. Очевидно, по замыслу предлагалось для класса иметь два поля Weight, одно для корпуса летательного аппарата и одно для двигателя. Этого можно достичь за счет переименования двух унаследованных объектов:
class Airplane inherit
Winged_Vehicle
rename Weight as Airframe_Weight;
Motorized_Vehicle
rename Weight as Engine_Weight;
…
end;
Предположим теперь, что мы хотим заместить подпрограмму display. Мы не можем использовать redefine, потому что при этом возникла бы неоднозначность указания подпрограммы, которую мы переопределяем. Решение состоит в том, чтобы использовать undefine для отмены определений обеих унаследованных подпрограмм и написать новую:
class Airplane inherit
Winged_Vehicle
undefine display end;
Motorized_Vehicle
undefine display end;
feature
display is... end;
end;
В справочном руководстве по языку Eiftel подробно обсуждается использование rename, redefine и undefine для разрешения неоднозначности при множественном наследовании.
15.5. Проектные соображения
Наследование и композиция
Наследование — это только один метод структурирования, который может использоваться в объектно-ориентированном проектировании. Более простым методом является композиция, которая представляет собой вложение одной абстракции внутрь другой. Вы уже знакомы с композицией, поскольку вам известно, что одна запись может быть включена внутрь другой:
with Airplane_Package;
package SS"f.Package is
type SST_Data is private;
private
type SST_Data is
record
A: Airplane. Data;
Mach: Float;
end record;
end SST_Package;
и в языке C++ класс может включать экземпляр другого класса как элемент:
class SST_Data {
private:
Airplane_Data a;
float mach;
};
Композиция — более простая операция, чем наследование, потому что для ее поддержки не требуется никаких новых конструкций языка; любая поддержка инкапсуляции модуля автоматически дает вам возможности для композиции абстракций. Родовые единицы, которые в любом случае необходимы в языке с проверкой соответствия типов, также могут использоваться для формирования абстракций. Наследование, однако, требует сложной поддержки языка (теговых записей в языке Ada и виртуальных функций в языке C++) и дополнительных затрат при выполнении на динамическую диспетчеризацию.
Если вам нужна динамическая диспетчеризация, то вы должны, конечно, выбрать наследование, а не композицию. Однако, если динамической диспетчеризации нет, выбор зависит только от решения вопроса, какой метод дает «лучший» проект. Вспомните, что язык C++ требует, чтобы при создании базового класса вы решили, должна ли выполняться динамическая диспетчеризация, объявляя одну или несколько подпрограмм как виртуальные; эти и только эти подпрограммы будут участвовать в диспетчеризации. В языке Ada 95 динамическая диспетчеризация потенциально произойдет в любой подпрограмме, объявленной с управляющим параметром тегового типа:
type T is tagged ...;
procedure Proc(Parm: T);
Фактически решение, является связывание статическим или динамическим, принимается отдельно для каждого вызова. Не используйте наследование, когда подошла бы простая запись.
Основное различие между двумя методами состоит в том, что композиция просто использует существующую закрытую абстракцию, в то время как наследование знает о реализации абстракции. Пользователи закрытой абстракции защищены от изменения реализации. При использовании наследования базовые классы не могут изменяться без учета того, какие изменения это вызовет в производных классах.
С другой стороны, при каждом доступе к закрытой абстракции должна выполняться подпрограмма интерфейса, в то время как наследование разрешает эффективный прямой доступ производным классам. Кроме того, вы можете изменить реализацию в производном классе, в то время как в композиции ограничены использованием существующей реализации. Говоря кратко: легко «купить» и «продать» модули для композиции, в то время как наследование делает вас «партнером» разработчика модуля.
Нет никакой опасности при аккуратном и продуманном использовании любого метода; проблемы могут возникнуть, когда наследование используется беспорядочно, поскольку при этом может возникнуть слишком много зависимостей между компонентами программной системы. Мы оставляем подробное обсуждение относительных достоинств этих двух понятий специализированным работам по ООП. О преимуществах наследования см. книгу Мейера по конструированию объектно-ориентированного программного обеспечения (Meyer, Object-oriented Software Construction, Prentice-Hall International, 1988), особенно гл. 14 и 19. Сравните ее с точкой зрения предпочтения композиции, выраженной в статье J.P. Rosen, «What orientation should Ada objects take?» Communications of the ACM, 35(11), 1992, стр. 71—76.
Использование наследования
Удобно разделить случаи применения наследования на несколько категорий:
Подобие поведения. SST ведет себя как Airplane. Это простое применение наследования для совместного использования кода: операции, подходящие для Airplane, подходят для SST. Операции при необходимости могут быть замещены.
Полиморфная совместимость. Linked-Set (связанное множество) и Bit-Set (битовое множество) полиморфно совместимы с Set. Происходя от общего предка, множества, которые реализованы по-разному, могут быть обработаны с помощью одних и тех же операций. Кроме того, вы можете создавать разнородные структуры данных, отталкиваясь от предка, который содержит элементы всего семейства типов.
Родовая совместимость. Общие свойства наследуются несколькими классами. Эта методика применяется в больших библиотеках, таких как в языках Smalltalk или Eiffel, где общие свойства выносятся в классы-предки, иногда называемые аспект-классами (aspect classes). Например, класс Comparable (сравнимый) мог бы использоваться для объявления таких операций отношения, как «<», и любой такой класс, как Integer или Float, обладающий такими операциями, наследуется из Comparable.
Подобие реализации. Класс может быть создан путем наследования логических функций из одного класса и их реализации — из другого. Классический пример — Bounded_Stack, который (множественно) наследует функциональные возможности из Stack и их реализации из Array. В более общем смысле, класс, созданный множественным наследованием, наследовал бы функциональные возможности из нескольких аспект-классов и реализацию из одного дополнительного класса.
Эти категории не являются ни взаимоисключающими, ни исчерпывающими; они представлены как руководство к использованию этой мощной конструкции в ваших программных проектах.
Перегрузка и полиморфизм
Хотя перегрузка (overloading) — это форма полиморфизма («многофор-менности»), эти две концепции применяются в совершенно разных целях. Перегрузка используется как удобное средство для задания одного и того же имени подпрограммам, которые функционируют на различных типах, в то время как динамический полиморфизм используется для реализации операции для семейства связанных типов. Например:
C++ |
void proc put(float);
представляет перегрузку, потому что общее имя используется только для удобства, и между int и float нет никакой связи. С другой стороны:
C++ |
является одной подпрограммой, которая может быть реализована по-разному для разных типов самолетов.
Технически трудно совместить перегрузку и динамический полиморфизм и не рекомендуется использовать эти два понятия вместе. Не пытайтесь внутри порожденного класса перегружать подпрограмму, которая появляется в базовом классе:
C++ |
public:
void set_speed(float); //float, а не int
};
Правила языка C++ определяют, что эта подпрограмма и не перегружает, и не замещает подпрограмму в базовом классе; вместо этого она скрывает определение в базовом классе точно так же, как внутренняя область действия!
Язык Ada 95 допускает сосуществование перегрузки и замещения :
with Airplane_Package; use Airplane_Package;
package SST_Package is
Ada |
procedure Set_Speed(A: in out SST_Data; I: in Integer);
-- Замещает примитивную подпрограмму из Airplane_Package procedure Set_Speed(A: in out SST_Data; I: in Float);
-- Перегрузка, не подпрограмма-примитив
end SST_Package;
Поскольку нет примитивной подпрограммы Set_Speed с параметром Float для родительского типа, второе объявление — это просто самостоятельная подпрограмма, которая перегружает то же самое имя. Хотя это допустимо, этого следует избегать, потому что пользователь типа, скорее всего, запутается. Посмотрев только на SST_Package (и без комментариев!), вы не сможете сказать, какая именно подпрограмма замещается, а какая перегружается:
Ada |
begin
Set_Speed(A, 500); -- Правильно, диспетчеризуется
Set_Speed(A, 500.0); -- Ошибка, не может диспетчеризоваться!
end Proc;
15.6. Методы динамического полиморфизма
Мы заключаем эту главу подведением итогов по динамическому полиморфизму в языках для объектно-ориентированного программирования.
Smalltalk. Каждый вызов подпрограммы требует динамической диспетчеризации, которая включает поиск по иерархии наследования, пока подпрограмма не будет найдена.
Eiftel. Каждый вызов подпрограммы диспетчеризуется динамически (если оптимизация не привела к статическому связыванию). В отличие от языка Smalltalk, возможные замещения известны во время компиляции, поэтому диспетчеризация имеет фиксированные издержки, вносимые таблицей переходов.
C++. Подпрограммы, которые явно объявлены виртуальными и вызываются косвенно через указатель или ссылку, диспетчеризуются динамически. Диспетчеризация во время выполнения имеет фиксированные издержки.
Ada 95. Динамическая диспетчеризация неявно используется для примитивных подпрограмм тегового типа, когда фактический параметр является CW-типом, а формальный параметр имеет конкретный тип. Затраты на диспетчеризацию во время выполнения фиксированы.
Языки отличаются деталями программирования и затратами, требующимися для динамического полиморфизма, и это влияет на стиль программирования и эффективность программ. Ясное понимание заложенных в языках принципов поможет вам сравнивать объектно-ориентированные языки и разрабатывать и создавать хорошие объектно-ориентированные программы на любом языке, который вы выберете.
15.7. Упражнения
1. Реализуйте пакеты на языке Ada 95 и классы на языке C++ для работы с множествами.
2. Может ли абстрактный тип в языке Ada 95 или абстрактный класс в языке C++ иметь компоненты-данные? Если так, для чего они могли бы использоваться?
type Item is abstract tagged
Ada |
I: Integer;
end record;
3. Напишите программу для неоднородной очереди, основываясь на абстрактном классе.
4. Реализуйте пакеты/классы для множеств с родовым типом элемента, а не только для целочисленных элементов.
5. Подробно изучите множественное наследование в языке Eiffel и сравните его с множественным наследованием в языке C++.
6. Стандартный пример множественного наследования в языке Eiffel -список фиксированного размера, реализованный с помощью наследования, как от списка, так и от массива. Как бы вы написали такие ADT (абстрактные типы данных) на языке Ada 95, в котором нет множественного наследования?
7. Чем опасно определение защищенных (protected) данных в языке C++? Относится ли это также к пакетам-детям в языке Ada 95?
-
Изучите структуру стандартной библиотеки в языке Ada 95, в котором широко используются пакеты-дети. Сравните ее со структурой стандартных классов ввода-вывода в языке C++.
9. Изучите пакет Finalization в языке Ada 95, который может использоваться для написания конструкторов и деструкторов. Сравните его с конструкциями языка C++.
10. Какова связь между операторами присваивания и конструкторами/де структорами?