Бьерн Страуструп. Язык программирования С++


Каркас области приложения



бет121/124
Дата16.07.2016
өлшемі3.27 Mb.
#204081
түріКнига
1   ...   116   117   118   119   120   121   122   123   124

13.7 Каркас области приложения


Мы перечислили виды классов, из которых можно создать библиотеки,

нацеленные на проектирование и повторное использование прикладных

программ. Они предоставляют определенные "строительные блоки" и

объясняют как из них строить. Разработчик прикладного обеспечения создает

каркас, в который должны вписаться универсальные строительные блоки. Задача

проектирования прикладных программ может иметь иное, более обязывающее

решение: написать программу, которая сама будет создавать общий каркас

области приложения. Разработчик прикладного обеспечения

в качестве строительных блоков будет встраивать в этот каркас

прикладные программы. Классы, которые образуют каркас области

приложения, имеют настолько обширный интерфейс, что их трудно

назвать типами в обычном смысле слова. Они приближаются к тому

пределу, когда становятся чисто прикладными классами, но при этом

в них фактически есть только описания, а все действия задаются

функциями, написанными прикладными программистами.

Для примера рассмотрим фильтр, т.е. программу, которая может

выполнять следующие действия: читать входной поток, производить

над ним некоторые операции, выдавать выходной поток и определять

конечный результат. Примитивный каркас для фильтра будет состоять

из определения множества операций, которые должен реализовать

прикладной программист:


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) или интерфейс с особыми библиотеками С++.





Достарыңызбен бөлісу:
1   ...   116   117   118   119   120   121   122   123   124




©dereksiz.org 2024
әкімшілігінің қараңыз

    Басты бет