logo search
Языки программирования

15.1. Структурированные классы

Абстрактные классы

Когда класс порождается из базового класса, предполагается, что базовый класс содержит большую часть требуемых данных и операций, тогда как производный класс всего лишь добавляет дополнительные данные, а также добавляет или изменяет некоторые операции. Во многих проектах лучше рас­сматривать базовый класс как некий каркас, определяющий общие операции для всего семейства производных классов. Например, семейство классов опе­раций ввода/вывода или графики может определять такие общие операции, как get и display, которые будут определены для каждого производного клас­са. И Ada 95, и C++ поддерживают такие абстрактные классы.

Мы продемонстрируем абстрактные классы, описывая несколько реализа­ций одной и той же абстракции; абстрактный класс будет определять структу­ру данных Set, и производные классы — реализовывать множества двумя раз­личными способами. В языке Ada 95 слово abstract обозначает абстрактный тип и абстрактные подпрограммы, связанные с этим типом:

Ada

package Set_Package is

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

private

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

begin

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

function lntersection(S1, S2: Set) return Set;

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

procedure Main is

S1.S2, S3: Set;

begin

S1 := Union(S2, S3);

end Main;

В C++ абстрактный класс создается с помощью объявления чистой виртуаль­ной функции, обозначенной «начальным значением» 0 для функции.

Абстрактный класс для множеств в языке C++ выглядит следующим обра­зом:

class Set {

C++

public:

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++

private:

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++

Bit_Setb1,b2, bЗ;

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

package List_Package is

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

package Integer_list is new List_Package(lnteger, Integer."<");

Конкретизация создает новый тип, и можно объявлять и использовать объекты этого типа:

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

generic

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

template <class ltem>

class List {

void put(const Item &);

};

Как только шаблон класса определен, вы можете определять объекты этого класса, задавая параметр шаблона:

C++

List<int>lnt_List1;

// lnt_List1 является экземпляром класса List с параметром int

Так же как и язык Ada, C++ позволяет программисту для объектов-экземп­ляров класса задать свои программы (процесс называется специализацией, spe­cialization) или воспользоваться по умолчанию подпрограммами, которые су­ществуют для класса. Есть важное различие родовых пакетов Ada и шаблонов C++. В языке Ada конкретизация родового пакета, который определяет тип, даст вам конкретный пакет, содержащий конкретный тип. Чтобы получить объект, потребуется еще один шаг. В C++ конкретизация дает объект сразу, не определяя конкретного класса. Чтобы определить другой объект, нужно просто конкретизировать шаблон снова:

C++

List<int>Int_List2; //Другой объект

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

Следующее различие между языками состоит в том, что C++ не использу­ет модель контракта, поэтому не исключено, что конкретизация вызовет ошибку компиляции в самом шаблоне (см. раздел 10.3).

Множественное наследование

Ранее обсуждалось порождение классов от одного базового класса, так что се­мейство классов образовывало дерево. При объектно-ориентированном про­ектировании, вероятно, класс будет иметь характеристики двух или несколь­ких существующих классов, и кажется допустимым порождать класс из не­скольких базовых классов. Это называется множественным наследованием (multiple inheritance). На рисунке 15.1 показано, что Airplane (самолет) может

быть многократно порожден из Winged_Vehicle (летательный аппарат с крыльями) и Motorized_Vehicle (летательный аппарат с мотором), в то время как Winged_Vehicle также является (единственным) базовым классом для Glider (планер). Задав два класса:

class Winged_Vehicle {

public:

void display(int);

C++

protected:

int Wing_Length; // Размах крыла

int Weight; // Bec

};

class Motorized_Vehicle {

public:

void display(int);

protected:

int Power; // Мощность

int Weight; // Bec

};

можно породить класс с помощью множественного наследования:

class Airplane:

C++

public Winged_Vehicle, public Motorized_Vehicle {

public:

void display_all();

};

Чтобы использовать множественное наследование, необходимо решить, что делать с данными и операциями, такими как Weight и display, которые насле­дуются из нескольких базовых классов. В языке C++ неоднозначность, вы­званная многократно определенными компонентами, должна быть явно раз­решена с помощью операции уточнения области действия:

void Airplane: :display_all()

{

C++

Winged_Vehicle::display(Wing_Length);

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 можно использовать для создания новых абстракций, комбинируя уже суще­ствующие абстракции. Очевидно, что наличие возможности множественного наследования оказывает глубокое влияние на проектирование и программи­рование объектно-ориентированной системы. Таким образом, трудно гово­рить об объектно-ориентированном проекте, не зависящем от языка; даже на самых ранних стадиях проектирования вам следует ориентироваться на конк­ретный язык программирования.