Мы перечислили виды классов, из которых можно создать библиотеки,
нацеленные на проектирование и повторное использование прикладных
программ. Они предоставляют определенные "строительные блоки" и
объясняют как из них строить. Разработчик прикладного обеспечения создает
каркас, в который должны вписаться универсальные строительные блоки. Задача
проектирования прикладных программ может иметь иное, более обязывающее
решение: написать программу, которая сама будет создавать общий каркас
области приложения. Разработчик прикладного обеспечения
в качестве строительных блоков будет встраивать в этот каркас
прикладные программы. Классы, которые образуют каркас области
приложения, имеют настолько обширный интерфейс, что их трудно
назвать типами в обычном смысле слова. Они приближаются к тому
пределу, когда становятся чисто прикладными классами, но при этом
в них фактически есть только описания, а все действия задаются
функциями, написанными прикладными программистами.
Для примера рассмотрим фильтр, т.е. программу, которая может
выполнять следующие действия: читать входной поток, производить
над ним некоторые операции, выдавать выходной поток и определять
конечный результат. Примитивный каркас для фильтра будет состоять
из определения множества операций, которые должен реализовать
прикладной программист:
class filter {
public:
class Retry {
public:
virtual const char* message() { return 0; }
};
virtual void start() { }
virtual int retry() { return 2; }
virtual int read() = 0;
virtual void write() { }
virtual void compute() { }
virtual int result() = 0;
};
Нужные для производных классов функции описаны как чистые виртуальные,
остальные функции просто пустые. Каркас содержит основной цикл
обработки и зачаточные средства обработки ошибок:
int main_loop(filter* p)
{
for (;;) {
try {
p->start();
while (p->read()) {
p->compute();
p->write();
}
return p->result();
}
catch (filter::Retry& m) {
cout << m.message() << '\n';
int i = p->retry();
if (i) return i;
}
catch (...) {
cout << "Fatal filter error\n";
return 1;
}
}
}
Теперь прикладную программу можно написать так:
class myfilter : public filter {
istream& is;
ostream& os;
char c;
int nchar;
public:
int read() { is.get(c); return is.good(); }
void compute() { nchar++; };
int result()
{ os << nchar
<< "characters read\n";
return 0;
}
myfilter(istream& ii, ostream& oo)
: is(ii), os(oo), nchar(0) { }
};
и вызывать ее следующим образом:
int main()
{
myfilter f(cin,cout);
return main_loop(&f);
}
Настоящий каркас, чтобы рассчитывать на применение в реальных задачах,
должен создавать более развитые структуры и предоставлять больше
полезных функций, чем в нашем простом примере. Как правило, каркас
образует дерево узловых классов. Прикладной программист поставляет
только классы, служащие листьями в этом многоуровневом дереве,
благодаря чему достигается общность между различными прикладными
программами и упрощается повторное использование полезных функций,
предоставляемых каркасом. Созданию каркаса могут способствовать
библиотеки, в которых определяются некоторые полезные классы, например,
такие как scrollbar ($$12.2.5) и dialog_box ($$13.4). После определения
своих прикладных классов программист может использовать эти классы.
13.8 Интерфейсные классы
Про один из самых важных видов классов обычно забывают - это "скромные"
интерфейсные классы. Такой класс не выполняет какой-то большой
работы, ведь иначе, его не называли бы интерфейсным. Задача
интерфейсном класса приспособить некоторую полезную функцию к
определенному контексту. Достоинство интерфейсных классов в том,
что они позволяют совместно использовать полезную функцию, не загоняя
ее в жесткие рамки. Действительно, невозможно рассчитывать, что функция
сможет сама по себе одинаково хорошо удовлетворить самые разные запросы.
Интерфейсный класс в чистом виде даже не требует генерации кода.
Вспомним описание шаблона типа Splist из $$8.3.2:
template
class Splist : private Slist {
public:
void insert(T* p) { Slist::insert(p); }
void append(T* p) { Slist::append(p); }
T* get() { return (T*) Slist::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) или интерфейс с особыми библиотеками С++.
Достарыңызбен бөлісу: |