17.1.1. Объектно-ориентированное проектирование

17.1.1. Объектно-ориентированное проектирование

Из чего складывается объектно-ориентированное проектирование четырех рассмотренных выше видов запросов? Как решаются проблемы их внутреннего представления?

С помощью наследования можно определить взаимосвязи между независимыми классами запросов. Для этого мы вводим в рассмотрение абстрактный класс Query, который будет служить для них базовым (соответственно сами эти классы будут считаться производными). Абстрактный класс можно представить себе как неполный, который становится более или менее завершенным, когда из него порождаются производные классы, - в нашем случае AndQuery, OrQuery, NotQuery и NameQuery.

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

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

_rop-eval();

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

Когда мы говорим о полиморфизме в языке C++, то имеем в виду главным образом способность указателя или ссылки на базовый класс адресовать любой из производных от него. Если определить обычную функцию eval() следующим образом:

// pquery может адресовать любой из классов, производных от Query

void eval( const Query *pquery )

{

pquery-eval();

}

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

int main()

{

AndQuery aq;

NotQuery notq;

OrQuery *oq = new OrQuery;

NameQuery nq( "Botticelli" );

// правильно: любой производный от Query класс

// компилятор автоматически преобразует в базовый класс

eval( &aq );

eval( &notq );

eval( oq );

eval( &nq );

}

В то же время попытка передать eval() адрес объекта класса, не являющегося производным от Query, вызовет ошибку компиляции:

int main()

{

string name( " Scooby-Doo" );

// ошибка: тип string не является производным от Query

eval( &name );

}

Внутри eval() выполнение инструкции вида

pquery-eval();

должно вызывать нужную виртуальную функцию-член eval() в зависимости от фактического класса объекта, адресуемого указателем pquery. В примере выше pquery последовательно адресует объекты AndQuery, NotQuery, OrQuery и NameQuery. В каждой точке вызова определяется фактический тип класса объекта и вызывается подходящий экземпляр eval().

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

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

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

// полиморфизма нет

int *pi;

// нет поддержанного языком полиморфизма

void *pvi;

// pquery может адресовать объект любого производного от Query класса

Query *pquery;

В C++ полиморфизм существует только в пределах отдельных иерархий классов. Указатели типа void* можно назвать полиморфными, но в языке их поддержка не предусмотрена. Такими указателями программист должен управлять самостоятельно, с помощью явных приведений типов и той или иной формы дискриминанта, показывающего, объект какого типа в данный момент адресуется. (Можно сказать, что это "второсортные" полиморфные объекты.)

Язык C++ обеспечивает поддержку полиморфизма следующими способами:

1. путем неявного преобразования указателя или ссылки на производный класс к указателю или ссылке на открытый базовый:

Query *pquery = new NameQuery( " Class" );

1. через механизм виртуальных функций:

pquery-eval();

с помощью операторов dynamic_cast и typeid (они подробно обсуждаются в разделе 19.1):

if ( NameQuery *pnq =

dynamic_cast NameQuery* ( pquery )) ...

Проблему представления запроса мы решим, определив каждый операнд в классах AndQuery, NotQuery и OrQuery как указатель на тип Query*. Например:

class AndQuery {

public:

// ...

private:

Query *_lop;

Query *_rop;

};

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

_rop-eval();

На рис. 17.1 показана иерархия наследования, состоящая из абстрактного класса Query и четырех производных от него классов. Как этот рисунок транслируется в код программы на C++?

Query AndQuery OrQuery NotQuery NameQuery

Рис. 17.1. Иерархия классов Query

В разделе 2.4 мы рассматривали реализацию иерархии классов IntArray. Синтаксическая структура определения иерархии, изображенной на рис. 17.1, аналогична:

class Query { ... };

class AndQuery : public Query { ... };

class OrQuery : public Query { ... };

class NotQuery : public Query { ... };

class NameQuery : public Query { ... };

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

: уровень-доступа базовый-класс

где уровень-доступа - это одно из ключевых слов public, protected, private (смысл защищенного и закрытого наследования мы обсудим в разделе 18.3), а базовый-класс - имя ранее определенного класса. Например, Query является открытым базовым классом для любого из четырех классов запросов.

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

// ошибка: Query должен быть определен

class Query;

class NameQuery : piblic Query { ... };

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

// ошибка: опережающее объявление не должно

// включать списка базовых классов

class NameQuery : public Query;

Правильный вариант в данном случае выглядит так:

// опережающее объявление как производного,

// так и обычного класса содержит только имя класса

class Query;

class NameQuery;

Главное различие между базовыми классами Query и IntArray (см. раздел 2.4) состоит в том, что Query не представляет никакого реального объекта в нашем приложении. Пользователи класса IntArray вполне могут определять и использовать объекты этого типа непосредственно. Что же касается Query, то разрешается определять лишь указатели и ссылки на него, используя их для косвенного манипулирования объектами производных классов. О Query говорят, что это абстрактный базовый класс. В противоположность этому IntArray является конкретным базовым классом. Преобладающей формой в объектно-ориентированном проектировании является определение абстрактного базового класса типа Query и одиночное открытое наследование ему.

Упражнение 17.1

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

книга аудио-книга

аудиокассета детская кукла

видеокассета видеоигра для приставки SEGA

книга с подневной оплатой видеоигра для приставки SONY

книга на компакт-диске видеоигра для приставки Nintendo

Упражнение 17.2

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

(a) Форматы графических файлов (gif, tiff, jpeg, bmp и т.д.)

(b) Геометрические примитивы (прямоугольник, круг, сфера, конус и т.д.)

(c) Типы языка C++ (класс, функция, функция-член и т.д.)