Бьерн Страуструп - Язык программирования С++. Главы 11-13
Страница 46. Узловые классы



13.4 Узловые классы

В действительности иерархия классов строится, исходя из совсем другой
концепции производных классов, чем концепция интерфейс-реализация,
которая использовалась для абстрактных типов. Класс рассматривается
как фундамент строения. Но даже, если в основании находится абстрактный
класс, он допускает некоторое представление в программе и сам предоставляет
для производных классов какие-то полезные функции. Примерами узловых
классов могут служить классы rectangle ($$6.4.2) и satellite ($$6.5.1).
Обычно в иерархии класс представляет некоторое общее понятие, а
производные классы представляют конкретные варианты этого понятия.
Узловой класс является неотъемлемой частью иерархии классов. Он пользуется
сервисом, представляемым базовыми классами, сам обеспечивает определенный
сервис и предоставляет виртуальные функции и (или) защищенный
интерфейс, чтобы позволить дальнейшую детализацию своих операций в
производных классах.
   Типичный узловой класс не только предоставляет реализацию
интерфейса, задаваемого его базовым классом (как это делает класс
реализации по отношению к абстрактному типу), но и сам расширяет
интерфейс, добавляя новые функции. Рассмотрим в качестве примера
класс dialog_box, который представляет окно некоторого вида на экране.
В этом окне появляются вопросы пользователю и в нем он задает свой
ответ с помощью нажатия клавиши или "мыши":

        class dialog_box : public window {
           // ...
        public:
           dialog_box(const char* ...); // заканчивающийся нулем список
                                        // обозначений клавиш
           // ...
        virtual int ask();
       };

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

        void user()
        {
          for (;;) {
              // какие-то команды

              dialog_box cont("continue",
                              "try again",
                              "abort",
                              (char*) 0);
              switch (cont.ask()) {
              case 0:  return;
              case 1:  break;
              case 2:  abort();
              }
            }
          }

Обратим внимание на использование конструктора. Конструктор, как
правило, нужен для узлового класса и часто это нетривиальный
конструктор. Этим узловые классы отличаются от абстрактных классов,
для которых редко нужны конструкторы.
   Пользователь класса dialog_box ( а не только создатель этого
класса) рассчитывает на сервис, представляемый его базовыми классами.
В рассматриваемом примере предполагается, что существует
некоторое стандартное размещение нового окна на экране. Если
пользователь захочет управлять размещением окна, базовый для
dialog_box класс window (окно) должен предоставлять такую возможность,
например:

          dialog_box cont("continue","try again","abort",(char*)0);
          cont.move(some_point);

Здесь функция движения окна move() рассчитывает на определенные
функции базовых классов.
    Сам класс dialog_box является хорошим кандидатом для построения
производных классов. Например, вполне разумно иметь такое окно,
в котором, кроме нажатия клавиши или ввода с мышью, можно задавать
строку символов (скажем, имя файла). Такое окно dbox_w_str строится
как производный класс от простого окна dialog_box:

          class dbox_w_str : public dialog_box {
              // ...
          public:
             dbox_w_str (
              const char* sl,  // строка запроса пользователю
              const char* ...  // список обозначений клавиш
             );
             int ask();
             virtual char* get_string();
             //...
          };

 Функция get_string() является той операцией, с помощью
которой программист получает заданную пользователем строку. Функция
ask() из класса dbox_w_str гарантирует, что строка введена правильно,
а если пользователь не стал вводить строку, то тогда в программу
возвращается соответствующее значение (0).

          void user2()
          {
            // ...
            dbox_w_str file_name("please enter file name",
                                 "done",
                                 (char*)0);
            file_name.ask();
            char* p = file_name.get_string();
            if (p) {
               // используем имя файла
            }
            else {
              // имя файла не задано
            }
            //
          }

Подведем итог - узловой класс должен:
   [1] рассчитывать на свои базовые классы как для их реализации,
       так и для представления сервиса пользователям этих классов;
   [2] представлять более полный интерфейс (т.е. интерфейс с большим
       числом функций-членов) пользователям, чем базовые классы;
   [3] основывать в первую очередь (но не исключительно) свой
       общий интерфейс на виртуальных функциях;
   [4] зависеть от всех своих (прямых и косвенных) базовых классов;
   [5] иметь смысл только в контексте своих базовых классов;
   [6] служить базовым классом для построения производных классов;
   [7] воплощаться в объекте.
Не все, но многие, узловые классы будут удовлетворять условиям
1, 2, 6 и 7. Класс, который не удовлетворяет условию 6, походит
на конкретный тип и может быть назван конкретным узловым классом.
Класс, который не удовлетворяет условию 7, походит на абстрактный
тип и может быть назван абстрактным узловым классом. У многих
узловых классов есть защищенные члены, чтобы предоставить для
производных классов менее ограниченный интерфейс.
   Укажем на следствие условия 4: для трансляции своей программы
пользователь узлового класса должен включить описания всех его
прямых и косвенных базовых классов, а также описания
всех тех классов, от которых, в свою очередь, зависят базовые классы.
В этом узловой класс опять представляет контраст с абстрактным типом.
Пользователь абстрактного типа не зависит от всех классов,
использующихся для реализации типа и для трансляции своей программы
не должен включать их описания.

 
« Предыдущая статья   Следующая статья »