Программирование >>  Поддержка объектно-ориентированного программирования 

1 ... 112 113 114 [ 115 ] 116 117 118 ... 120


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.messageO << \n; int i = p->retry(); if (i) return i;

catch (...) {

cout << Fatal filter error\n ; return 1;

Теперь прикладную программу можно написать так:



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 ($$1 3.4). После определения своих прикладных классов программист может использовать эти классы.

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* в более удобное семейство надежных классов, представляющих списки. Чтобы применение интерфейсных классов не было слишком накладно, нужно использовать функции-подстановки. В примерах, подобных приведенному, где задача функций-подстановок только подогнать тип, накладные расходы в памяти и скорости выполнения программы не возникают.

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

class myfilter : public filter { istream& is; ostream& os; char c; int nchar; public:

int read() { is.get(c); return is.goodO; } void compute() { nchar++; }; int result() { os << nchar

<< characters read\n ; return 0;



абстрактный тип, реализуемый конкретными типами ($$1 3.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();



1 ... 112 113 114 [ 115 ] 116 117 118 ... 120

© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки.
Яндекс.Метрика