Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Липпман.doc
Скачиваний:
7
Добавлен:
14.08.2019
Размер:
7.54 Mб
Скачать

17.1. Определение иерархии классов

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

NameQuery // Shakespeare

NotQuery // ! Shakespeare

OrQuery // Shakespeare || Marlowe


AndQuery // William && Shakespeare

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

Таким образом, запрос

untamed || fiery

состоит из объекта класса OrQuery, который содержит два объекта NameQuery в качестве операндов. Для простых запросов этого достаточно, но при обработке составных запросов типа

Alice || Emma && Weeks

возникает проблема. Данный запрос состоит из двух подзапросов: объекта OrQuery, содержащего объекты NameQuery для представления слов Alice и Emma, и объекта AndQuery. Правым операндом AndQuery является объект NameQuery для слова Weeks.

AndQuery

OrQuery

NameQuery ("Alice")

NameQuery ("Emma")


NameQuery ("Weeks")

Но левый операнд – это объект OrQuery, предшествующий оператору &&. На его месте мог бы быть объект NotQuery или другой объект AndQuery. Как же следует представить операнд, если он может принадлежать к типу любого из четырех классов? Эта проблема имеет две стороны:

  • необходимо уметь объявлять тип операнда в классах OrQuery, AndQuery и NotQuery так, чтобы с его помощью можно было представить тип любого из четырех классов запросов;

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

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

// не объектно-ориентированное решение

union op_type {

// объединение не может содержать объекты классов с

// ассоциированными конструкторами

NotQuery *nq;

OrQuery *oq;

AndQuery *aq;

string *word;

};

enum opTypes {

Not_query=1, O_query, And_query, Name_query

};

class AndQuery {

public:

// ...

private:

/*

* opTypes хранит информацию о фактических типах операндов запроса

* op_type - это сами операнды

*/

op_type _lop, _rop;

opTypes _lop_type, _rop_type;


};

Хранить указатели на объекты можно и с помощью типа void*:

class AndQuery {

public:

// ...

private:

void * _lop, _rop;

opTypes _lop_type, _rop_type;


};

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

Основной недостаток рассмотренных решений состоит в том, что ответственность за определение типа возлагается на программиста. Например, в случае решения, основанного на void*-указателях, операцию eval() для объекта AndQuery можно реализовать так:

void

AndQuery::

eval()

{

// не объектно-ориентированный подход

// ответственность за разрешение типа ложится на программиста

// определить фактический тип левого операнда

switch( _lop_type ) {

case And_query:

AndQuery *paq = static_cast<AndQuery*>(_lop);

paq->eval();

break;

case Or_query:

OrQuery *pqq = static_cast<OrQuery*>(_lop);

poq->eval();

break;

case Not_query:

NotQuery *pnotq = static_cast<NotQuery*>(_lop);

pnotq->eval();

break;

case Name_query:

AndQuery *pnmq = static_cast<NameQuery*>(_lop);

pnmq->eval();

break;

}

// то же для правого операнда


}

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

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

// объектно-ориентированное решение

// ответственность за разрешение типов перекладывается на компилятор

// примечание: теперь _lop и _rop - объекты типа класса

// их определения будут приведены ниже

void

AndQuery::

eval()

{

_lop->eval();

_rop->eval();


}

Если потребуется добавить или исключить какие-либо типы, эту часть программы не придется ни переписывать, ни перекомпилировать.