Язык программирования C++. Вводный курс


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


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

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();
}

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


Содержание раздела