15.1. Структурированные классы
Абстрактные классы
Когда класс порождается из базового класса, предполагается, что базовый класс содержит большую часть требуемых данных и операций, тогда как производный класс всего лишь добавляет дополнительные данные, а также добавляет или изменяет некоторые операции. Во многих проектах лучше рассматривать базовый класс как некий каркас, определяющий общие операции для всего семейства производных классов. Например, семейство классов операций ввода/вывода или графики может определять такие общие операции, как get и display, которые будут определены для каждого производного класса. И Ada 95, и C++ поддерживают такие абстрактные классы.
Мы продемонстрируем абстрактные классы, описывая несколько реализаций одной и той же абстракции; абстрактный класс будет определять структуру данных Set, и производные классы — реализовывать множества двумя различными способами. В языке Ada 95 слово abstract обозначает абстрактный тип и абстрактные подпрограммы, связанные с этим типом:
Ada |
type Set is abstract tagged null record;
function Union(S1, S2: Set) return Set is abstract;
function Intersection(S1, S2: Set) return Set is abstract;
end Set_Package;
Вы не можете объявить объект абстрактного типа и не можете вызвать абстрактную подпрограмму. Тип служит только каркасом для порождения конкретных типов, а подпрограммы должны замещаться конкретными подпрограммами.
Сначала мы рассмотрим производный тип, в котором множество представлено булевым массивом:
with Set_Package;
package Bit_Set_Package is
type Set is new Set_Package.Set with private;
function Union(S1, S2: Set) return Set;
function lntersection(S1, S2: Set) return Set;
Ada |
type Bit_Array is array(1..100) of Boolean;
type Set is new Set_Package.Set with
record
Data: Bit_Array;
end record;
end Bit_Set_Package;
Конечно, необходимо тело пакета, чтобы реализовать операции.
Производный тип — это конкретный тип с конкретными компонентами данных и операциями, и он может использоваться как любой другой тип:
with Bit_Set_Package; use Bit_Set_Package;
procedure Main is
S1.S2, S3: Set;
Ada |
S1 := Union(S2, S3);
end Main;
Предположим теперь, что в другой части программы требуется другая реализация множеств, которая использует связанные списки вместо массивов. Вы можете породить дополнительный конкретный тип из абстрактного типа и использовать его вместо или в дополнение к предыдущей реализации:
with Set_Package;
package Linked_Set_Package is
type Set is new Set_Package.Set with private;
function Union(S1, S2: Set) return Set;
Ada |
private
type Node;
type Pointer is access Node;
type Set is new Set_Package.Set with
record
Head: Pointer;
end record;
end Linked_Set_Package;
Новая реализация может использоваться другим модулем; фактически, вы можете изменить реализацию, используемую в существующих модулях, просто заменяя контекстные указания:
Ada with Linked_Set_Package; use Linked_Set_Package;
Ada |
S1.S2, S3: Set;
begin
S1 := Union(S2, S3);
end Main;
В C++ абстрактный класс создается с помощью объявления чистой виртуальной функции, обозначенной «начальным значением» 0 для функции.
Абстрактный класс для множеств в языке C++ выглядит следующим образом:
class Set {
C++ |
virtual void Union(Set&, Set&) = 0;
virtual void lntersection(Set&, Set&) = 0;
};
У абстрактных классов не бывает экземпляров; абстрактный класс может только быть базовым для производных классов:
class Bit_Set: public Set {
public:
virtual void Union(Set&, Set&);
virtual void lntersection(Set&, Set&);
C++ |
int data[100];
};
class Linked_Set: public Set {
public:
virtual void Union(Set&, Set&);
virtual void lntersection(Set&, Set&);
private:
int data;
Set *next;
};
Конкретные производные классы можно использовать как любой другой класс: __
void proc()
{
C++ |
Linked_Set 11,12,l3;
b1.Union(b2,b3);
H.Union(l2,I3);
}
Обратите внимание на разницу в синтаксисе двух языков, которая вызвана разными подходами к ООП. В языке Ada 95 определяется обычная функция, которая получает два множества и возвращает третье. В языке C++ одно из множеств — отличимый получатель сообщения. Для
b1.Union(b2,b3);
подразумевается, что экземпляр b1, отличимый получатель операции Union, получит результат операции от двух параметров — Ь2 и bЗ — и использует его, • чтобы заменить текущее значение внутренних данных.
Возможно, вы предпочтете перегрузить предопределенные операции, например «+» и «*», вместо того чтобы использовать имена Union и Intersection. Это можно сделать как в C++, так и в Ada 95.
Все реализации абстрактного класса покрываются типом класса (CW-типом) Set'Class. Величины абстрактного CW-типа будут диспетчеризованы к правильному конкретному типу, т. е. к правильной реализации. Таким образом, абстрактные типы и операции дают возможность программисту писать программное обеспечение, не зависящее от реализации.
Родовые возможности
В разделе 10.3 мы обсуждали родовые подпрограммы в языке Ada, которые позволяют программисту создавать шаблоны подпрограмм и затем конкретизировать их для различных типов. Родовые возможности чаще всего находят приложение в пакетах Ada; например, пакет работы со списком может быть родовым в отношении типа элементов списка. Кроме того, он может быть родовым в отношении функций, сравнивающих элементы, с тем
чтобы элементы списка можно было сортировать:
generic
type Item is private;
with function "<"(X, Y: in Item) return Boolean;
Ada |
type List is private;
procedure Put(l: in Item; L: in out List);
procedure Get(l: out Item; L: in out List);
private
type List is array( 1.. 100) of Item;
end List_Package;
Этот пакет теперь может быть конкретизирован для любого типа элемента:
Ada |
Конкретизация создает новый тип, и можно объявлять и использовать объекты этого типа:
lnt_List_1, lnt_List_2: lnteger_List.List;
lnteger_List.Put(42, lnt_List_1 );
lnteger_List.Put(59, lnt_List_2);
В языке Ada есть богатый набор нотаций для написания родовых формальных параметров, которые используются в модели контракта, чтобы ограничить фактические параметры некоторыми классами типов, такими как дискретные типы или типы с плавающей точкой. В языке Ada 95 эти средства обобщены до возможности специфицировать в родовом формальном параметре классы типов, задаваемые программистом:
with Set_Package;
Ada |
type Set_Class is new Set_Package.Set; package Set_IO is
…
end Set_IO;
Эта спецификация означает, что родовой пакет может быть конкретизирован с любым типом, производным от тегового типа Set, такого как Bit_Set и Linked_Set. Все операции из Set, такие как Union, могут использоваться внутри родового пакета, потому что из модели контракта мы знаем, что любая конкретизация будет с типом, производным от Set, и, следовательно, она наследует или замещает эти операции.
Шаблоны
В языке C++ можно определять шаблоны классов:
Ada |
class List {
void put(const Item &);
};
Как только шаблон класса определен, вы можете определять объекты этого класса, задавая параметр шаблона:
C++ |
// lnt_List1 является экземпляром класса List с параметром int
Так же как и язык Ada, C++ позволяет программисту для объектов-экземпляров класса задать свои программы (процесс называется специализацией, specialization) или воспользоваться по умолчанию подпрограммами, которые существуют для класса. Есть важное различие родовых пакетов Ada и шаблонов C++. В языке Ada конкретизация родового пакета, который определяет тип, даст вам конкретный пакет, содержащий конкретный тип. Чтобы получить объект, потребуется еще один шаг. В C++ конкретизация дает объект сразу, не определяя конкретного класса. Чтобы определить другой объект, нужно просто конкретизировать шаблон снова:
C++ |
Компилятор и компоновщик отвечают за то, чтобы отследить пути всех конкретизации одного и того же типа и гарантировать, что код для операций шаблона класса не тиражируется для каждого объекта.
Следующее различие между языками состоит в том, что C++ не использует модель контракта, поэтому не исключено, что конкретизация вызовет ошибку компиляции в самом шаблоне (см. раздел 10.3).
Множественное наследование
Ранее обсуждалось порождение классов от одного базового класса, так что семейство классов образовывало дерево. При объектно-ориентированном проектировании, вероятно, класс будет иметь характеристики двух или нескольких существующих классов, и кажется допустимым порождать класс из нескольких базовых классов. Это называется множественным наследованием (multiple inheritance). На рисунке 15.1 показано, что Airplane (самолет) может
быть многократно порожден из Winged_Vehicle (летательный аппарат с крыльями) и Motorized_Vehicle (летательный аппарат с мотором), в то время как Winged_Vehicle также является (единственным) базовым классом для Glider (планер). Задав два класса:
class Winged_Vehicle {
public:
void display(int);
C++ |
int Wing_Length; // Размах крыла
int Weight; // Bec
};
class Motorized_Vehicle {
public:
void display(int);
protected:
int Power; // Мощность
int Weight; // Bec
};
можно породить класс с помощью множественного наследования:
class Airplane:
C++ |
public:
void display_all();
};
Чтобы использовать множественное наследование, необходимо решить, что делать с данными и операциями, такими как Weight и display, которые наследуются из нескольких базовых классов. В языке C++ неоднозначность, вызванная многократно определенными компонентами, должна быть явно разрешена с помощью операции уточнения области действия:
void Airplane: :display_all()
{
C++ |
Winged_Vehicle::display(Winged_ Vehicle:: Weight);
Motorized_ Vehicle:: display(Power);
Motorized_ Vehicle:: display(Motorized_ Vehicle:: Weight);
};
Это нельзя считать удачным решением, так как вся идея наследования в том, чтобы допускался прямой доступ к данным и операциям базы, если не требуется их модификации. Реализовать множественное наследование намного труднее, чем простое наследование, которое мы описали в разделе 14.4. Более подробно см. разделы с 10.1с по 10.1с упомянутого ранее справочного руководства по языку C++.
Значение множественного наследования в ООП является предметом для дискуссии. Некоторые языки программирования, такие как Eiffel, поддерживают использование множественного наследования, в то время как языки, подобные Ada 95 и Smalltalk, не имеют таких средств. При этом утверждается, что проблемы, которые можно решить с помощью множественного наследования, изящно решаются с использованием других средств языка. Например, выше мы отмечали, что родовые параметры теговых типов в языке Ada 95 можно использовать для создания новых абстракций, комбинируя уже существующие абстракции. Очевидно, что наличие возможности множественного наследования оказывает глубокое влияние на проектирование и программирование объектно-ориентированной системы. Таким образом, трудно говорить об объектно-ориентированном проекте, не зависящем от языка; даже на самых ранних стадиях проектирования вам следует ориентироваться на конкретный язык программирования.
- Глава 1
- 1.2. Процедурные языки
- 1.3. Языки, ориентированные на данные
- 1.4. Объектно-ориентированные языки
- 1.5. Непроцедурные языки
- 1.6. Стандартизация
- 1.7. Архитектура компьютера
- 1.8. Вычислимость
- 1.9. Упражнения
- Глава 2
- 2.2. Семантика
- 2.3. Данные
- 2.4. Оператор присваивания
- 2.5. Контроль соответствия типов
- 2.7. Подпрограммы
- 2.8. Модули
- 2.9. Упражнения
- Глава 3
- 3.1. Редактор
- 3.2. Компилятор
- 3.3. Библиотекарь
- 3.4. Компоновщик
- 3.5. Загрузчик
- 3.6. Отладчик
- 3.7. Профилировщик
- 3.8. Средства тестирования
- 3.9. Средства конфигурирования
- 3.10. Интерпретаторы
- 3.11. Упражнения
- Глава 4
- 4.1. Целочисленные типы
- I: Integer; -- Целое со знаком в языке Ada
- 4.2. Типы перечисления
- 4.3. Символьный тип
- 4.4. Булев тип
- 4.5. Подтипы
- 4.6. Производные типы
- 4.7. Выражения
- 4.8. Операторы присваивания
- 4.9. Упражнения
- Глава 5
- 5.1. Записи
- 5.2. Массивы
- 5.3. Массивы и контроль соответствия типов
- Подтипы массивов в языке Ada
- 5.5. Строковый тип
- 5.6. Многомерные массивы
- 5.7. Реализация массивов
- 5.8. Спецификация представления
- 5.9. Упражнения
- Глава 6
- 6.1. Операторы switch и case
- 6.2. Условные операторы
- 6.3. Операторы цикла
- 6.4. Цикл for
- 6.5. «Часовые»
- 6.6. Инварианты
- 6.7. Операторы goto
- 6.8. Упражнения
- Глава 7
- 7.1. Подпрограммы: процедуры и функции
- 7.2. Параметры
- 7.3. Передача параметров подпрограмме
- 7.4. Блочная структура
- 7.5. Рекурсия
- 7.6. Стековая архитектура
- 7.7. Еще о стековой архитектуре
- 7.8. Реализация на процессоре Intel 8086
- 7.9. Упражнения
- Глава 8
- 8.1 . Указательные типы
- 8.2. Структуры данных
- 8.3. Распределение памяти
- 8.4. Алгоритмы распределения динамической памяти
- 8.5. Упражнения
- Глава 9
- 9.1. Представление вещественных чисел
- 9.2. Языковая поддержка вещественных чисел
- 9.3. Три смертных греха
- Вещественные типы в языке Ada
- 9.5. Упражнения
- Глава 10
- 10.1. Преобразование типов
- 10.2. Перегрузка
- 10.3. Родовые (настраиваемые) сегменты
- 10.4. Вариантные записи
- 10.5. Динамическая диспетчеризация
- 10.6. Упражнения
- Глава 11
- 11.1. Требования обработки исключительных ситуаций
- 11.2. Исключения в pl/I
- 11.3. Исключения в Ada
- 11.5. Обработка ошибок в языке Eiffei
- 11.6. Упражнения
- Глава 12
- 12.1. Что такое параллелизм?
- 12.2. Общая память
- 12.3. Проблема взаимных исключений
- 12.4. Мониторы и защищенные переменные
- 12.5. Передача сообщений
- 12.6. Язык параллельного программирования оссаm
- 12.7. Рандеву в языке Ada
- 12.9. Упражнения
- Глава 13
- 13.1. Раздельная компиляция
- 13.2. Почему необходимы модули?
- 13.3. Пакеты в языке Ada
- 13.4. Абстрактные типы данных в языке Ada
- 13.6. Упражнения
- Глава 14
- 14.1. Объектно-ориентированное проектирование
- В каждом объекте должно скрываться одно важное проектное решение.
- 14.3. Наследование
- 14.5. Объектно-ориентированное программирование на языке Ada 95
- Динамический полиморфизм в языке Ada 95 имеет место, когда фактический параметр относится к cw-типу, а формальный параметр относится к конкретному типу.
- 14.6. Упражнения
- Глава 15
- 1. Структурированные классы.
- 15.1. Структурированные классы
- 5.2. Доступ к приватным компонентам
- 15.3. Данные класса
- 15.4. Язык программирования Eiffel
- Если свойство унаследовано от класса предка более чем одним путем, оно используется совместно; в противном случае свойства реплицируются.
- 15.5. Проектные соображения
- 15.6. Методы динамического полиморфизма
- 15.7. Упражнения
- 5Непроцедурные
- Глава 16
- 16.1. Почему именно функциональное программирование?
- 16.2. Функции
- 16.3. Составные типы
- 16.4. Функции более высокого порядка
- 16.5. Ленивые и жадные вычисления
- 16.6. Исключения
- 16.7. Среда
- 16.8. Упражнения
- Глава 17
- 17.2. Унификация
- 17.4. Более сложные понятия логического программирования
- 17.5. Упражнения
- Глава 18
- 18.1. Модель Java
- 18.2. Язык Java
- 18.3. Семантика ссылки
- 18.4. Полиморфные структуры данных
- 18.5. Инкапсуляция
- 18.6. Параллелизм
- 18.7. Библиотеки Java
- 8.8. Упражнения