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



13.8 Интерфейсные классы

Про один из самых важных видов классов обычно забывают - это "скромные"
интерфейсные классы. Такой класс не выполняет какой-то большой
работы, ведь иначе, его не называли бы интерфейсным. Задача
интерфейсном класса приспособить некоторую полезную функцию к
определенному контексту. Достоинство интерфейсных классов в том,
что они позволяют совместно использовать полезную функцию, не загоняя
ее в жесткие рамки. Действительно, невозможно рассчитывать, что функция
сможет сама по себе одинаково хорошо удовлетворить самые разные запросы.
   Интерфейсный класс в чистом виде даже не требует генерации кода.
Вспомним описание шаблона типа Splist из $$8.3.2:

          template<class T>
          class Splist : private Slist<void*> {
          public:
            void insert(T* p) { Slist<void*>::insert(p); }
            void append(T* p) { Slist<void*>::append(p); }
            T* get() { return (T*) Slist<void*>::get(); }
          };

Класс Splist преобразует список ненадежных обобщенных указателей
типа void* в более удобное семейство надежных классов, представляющих
списки. Чтобы применение интерфейсных классов не было слишком накладно,
нужно использовать функции-подстановки. В примерах, подобных
приведенному, где задача функций-подстановок только подогнать
тип, накладные расходы в памяти и скорости выполнения программы
не возникают.
   Естественно, можно считать интерфейсным абстрактный
базовый класс, который представляет абстрактный тип, реализуемый
конкретными типами ($$13.3), также как и управляющие классы
из раздела 13.9. Но здесь мы рассматриваем классы, у которых нет
иных назначений - только задача адаптации интерфейса.
   Рассмотрим задачу слияния двух иерархий классов с помощью
множественного наследования. Как быть в случае коллизии
имен, т.е. ситуации, когда в двух классах используются виртуальные
функции с одним именем, производящие совершенно разные операции?
Пусть есть видеоигра под названием "Дикий запад", в которой диалог
с пользователем организуется с помощью окна общего вида (класс
Window):

          class Window {
             // ...
             virtual void draw();
          };

          class Cowboy {
             // ...
             virtual void draw();
         };

         class CowboyWindow : public Cowboy, public Window {
            // ...
         };

В этой игре класс CowboyWindow представляет движение ковбоя на экране
и управляет взаимодействием игрока с ковбоем. Очевидно, появится
много полезных функций, определенных в классе Window и
Cowboy, поэтому предпочтительнее использовать множественное наследование,
чем описывать Window или Cowboy как члены. Хотелось бы передавать
этим функциям в качестве параметра объект типа CowboyWindow, не требуя
от программиста указания каких-то спецификаций объекта. Здесь
как раз и возникает вопрос, какую функции выбрать для CowboyWindow:
Cowboy::draw() или Window::draw().
    В классе CowboyWindow может быть только одна функция с именем
draw(), но поскольку полезная функция работает с объектами Cowboy
или Window и ничего не знает о CowboyWindow, в классе CowboyWindow
должны подавляться (переопределяться) и функция Cowboy::draw(), и
функция Window_draw(). Подавлять обе функции с помощью одной -
draw() неправильно, поскольку, хотя используется одно имя, все же
все функции draw() различны и не могут переопределяться одной.
    Наконец, желательно, чтобы в классе CowboyWindow наследуемые
функции Cowboy::draw() и Window::draw() имели различные однозначно
заданные имена.
    Для решения этой задачи нужно ввести дополнительные классы для
Cowboy и Window. Вводится два новых имени
для функций draw() и гарантируется, что их вызов
в классах Cowboy и Window приведет к вызову функций с новыми именами:

         class CCowboy : public Cowboy {
            virtual int cow_draw(int) = 0;
            void draw() { cow_draw(i); } // переопределение Cowboy::draw
         };

         class WWindow : public Window {
            virtual int win_draw() = 0;
            void draw() { win_draw(); } // переопределение Window::draw
         };

Теперь с помощью интерфейсных классов CCowboy и WWindow можно
определить класс CowboyWindow и сделать требуемые переопределения
функций cow_draw() и win_draw:

         class CowboyWindow : public CCowboy, public WWindow {
           // ...
           void cow_draw();
           void win_draw();
         };

Отметим, что в действительности трудность возникла лишь потому, что
у обеих функций draw()  одинаковый тип параметров. Если бы типы
параметров различались, то обычные правила разрешения неоднозначности
при перегрузке гарантировали бы, что трудностей не возникнет, несмотря на
наличие различных функций с одним именем.
    Для каждого случая использования интерфейсного класса можно
предложить такое расширение языка, чтобы требуемая адаптация
проходила более эффективно или задавалась более элегантным способом.
Но такие случаи являются достаточно редкими, и нет смысла чрезмерно
перегружать язык, предоставляя специальные средства для каждого
отдельного случая. В частности, случай коллизии имен при слиянии иерархий
классов довольно редки, особенно если сравнивать с
тем, насколько часто программист создает классы. Такие случаи
могут возникать при слиянии иерархий классов из разных
областей (как в нашем примере: игры и операционные системы).
Слияние таких разнородных структур классов всегда непростая задача,
и разрешение коллизии имен является в ней далеко не самой трудной
частью. Здесь возникают проблемы из-за разных стратегий обработки
ошибок, инициализации, управления памятью. Пример, связанный
с коллизией имен, был приведен потому, что предложенное решение:
введение интерфейсных классов с функциями-переходниками, - имеет
много других применений. Например, с их помощью можно менять
не только имена, но и типы параметров и возвращаемых значений,
вставлять определенные динамические проверки и т.д.
   Функции-переходники CCowboy::draw() и WWindow_draw являются
виртуальными, и простая оптимизация с помощью подстановки невозможна.
Однако, есть возможность, что транслятор распознает такие функции
и удалит их из цепочки вызовов.
   Интерфейсные функции служат для приспособления интерфейса к
запросам пользователя. Благодаря им в интерфейсе собираются операции,
разбросанные по всей программе. Обратимся к классу vector из $$1.4.
Для таких векторов, как и для массивов, индекс
отсчитывается от нуля. Если пользователь хочет работать с
диапазоном индексов, отличным от диапазона 0..size-1, нужно сделать
соответствующие приспособления, например, такие:

          void f()
          {
            vector v(10);  // диапазон [0:9]

            // как будто v в диапазоне [1:10]:

            for (int i = 1; i<=10; i++) {
               v[i-1] = ... // не забыть пересчитать индекс

            }
            // ...
          }

Лучшее решение дает класс vec c произвольными границами индекса:

          class vec : public vector {
            int lb;
          public:
            vec(int low, int high)
               : vector(high-low+1) { lb=low; }

            int& operator[](int i)
               { return vector::operator[](i-lb); }

            int low() { return lb; }
            int high() { return lb+size() - 1; }
          };

Класс vec можно использовать без дополнительных операций, необходимых
в первом примере:

          void g()
          {
            vec v(1,10);  // диапазон [1:10]

            for (int i = 1; i<=10; i++) {
                v[i] = ...

            }
            // ...
          }

Очевидно, вариант с классом vec нагляднее и безопаснее.
   Интерфейсные классы имеют и другие важные области применения,
например, интерфейс между программами на С++ и программами на другом
языке ($$12.1.4) или интерфейс с особыми библиотеками С++.

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