С++ для начинающих

       

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


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

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

    }

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


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