Б. Страуструп - Язык программирования С++ (1119446), страница 99
Текст из файла (страница 99)
пересечениемножеств операций, но такой интерфейс будет слишком узким. На самом деле, во многих, имеющих351Бьерн Страуструп.Язык программирования С++смысл случаях такое пересечение пусто. В качестве альтернативного решения можно предоставитьобъединение всех множеств операций и предусмотреть динамическую ошибку, когда в этоминтерфейсе к объекту применяется "несуществующая" операция.
Объединение интерфейсов классов,представляющих множество понятий, называется обширным интерфейсом. Опишем "общий" контейнеробъектов типа T:class container {public:struct Bad_operation { // класс особых ситуацийconst char* p;Bad_operation(const char* pp) : p(pp) { }};virtual void put(const T*){ throw Bad_operation("container::put"); }virtual T* get(){ throw Bad_operation("container::get"); }virtual T*& operator[](int){ throw Bad_operation("container::[](int)"); }virtual T*& operator[](const char*){ throw Bad_operation("container::[](char*)"); }// ...};Все-таки существует мало реализаций, где удачно представлены как индексирование, так и операциитипа списочных, и, возможно, не стоит совмещать их в одном классе.Отметим такое различие: для гарантии проверки на этапе трансляции в абстрактном типе используютсячистые виртуальные функции, а для обнаружения ошибок на этапе выполнения используются функцииобширного интерфейса, запускающие особые ситуации.Можно следующим образом описать контейнер, реализованный как простой список с одностороннейсвязью:class slist_container : public container, private slist {public:void put(const T*);T* get();T*& operator[](int){ throw Bad_operation("slist::[](int)"); }T*& operator[](const* char){ throw Bad_operation("slist::[](char*)"); }// ...};Чтобы упростить обработку динамических ошибок для списка введены операции индексирования.Можно было не вводить эти нереализованные для списка операции и ограничиться менее полнойинформацией, которую предоставляют особые ситуации, запущенные в классе container:class vector_container : public container, private vector {public:T*& operator[](int);T*& operator[](const char*);// ...};Если быть осторожным, то все работает нормально:void f(){slist_container sc;vector_container vc;// ...}352Бьерн Страуструп.void{T*T*////}Язык программирования С++user(container& c1, container& c2)p1 = c1.get();p2 = c2[3];нельзя использовать c2.get() или c1[3]...Все же для избежания ошибок при выполнении программы часто приходится использоватьдинамическую информацию о типе ($$13.5) или особые ситуации ($$9).
Приведем пример:void user2(container& c1, container& c2)/*обнаружение ошибки просто, восстановление - трудная задача*/{try {T* p1 = c1.get();T* p2 = c2[3];// ...}catch(container::Bad_operation& bad) {// Приехали!// А что теперь делать?}}или другой пример:void user3(container& c1, container& c2)/*обнаружение ошибки непросто,а восстановление по прежнему трудная задача*/{slist* sl = ptr_cast(slist_container,&c1);vector* v = ptr_cast(vector_container, &c2);if (sl && v) {T* p1 = c1.get();T* p2 = c2[3];// ...}else {// Приехали!// А что теперь делать?}}Оба способа обнаружения ошибки, показанные на этих примерах, приводят к программе с "раздутым"кодом и низкой скоростью выполнения.
Поэтому обычно просто игнорируют возможные ошибки внадежде, что пользователь на них не натолкнется. Но задача от этого не упрощается, ведь полноетестирование затруднительно и требует многих усилий.Поэтому, если целью является программа с хорошими характеристиками, или требуются высокиегарантии корректности программы, или, вообще, есть хорошая альтернатива, лучше не использоватьобширныеинтерфейсы. Кроме того, использованиеобширного интерфейса нарушаетвзаимнооднозначное соответствие между классами и понятиями, и тогда начинают вводить новыепроизводные классы просто для удобства реализации.353Бьерн Страуструп.Язык программирования С++13.7 Каркас области приложенияМы перечислили виды классов, из которых можно создать библиотеки, нацеленные на проектированиеи повторное использование прикладных программ. Они предоставляют определенные "строительныеблоки" и объясняют как из них строить.
Разработчик прикладного обеспечения создает каркас, вкоторый должны вписаться универсальные строительные блоки. Задача проектирования прикладныхпрограмм может иметь иное, более обязывающее решение: написать программу, которая сама будетсоздавать общий каркас области приложения. Разработчик прикладного обеспечения в качествестроительных блоков будет встраивать в этот каркас прикладные программы. Классы, которыеобразуют каркас области приложения, имеют настолько обширный интерфейс, что их трудно назватьтипами в обычном смысле слова.
Они приближаются к тому пределу, когда становятся чистоприкладными классами, но при этом в них фактически есть только описания, а все действия задаютсяфункциями, написанными прикладными программистами.Для примера рассмотрим фильтр, т.е. программу, которая может выполнять следующие действия:читать входной поток, производить над ним некоторые операции, выдавать выходной поток иопределять конечный результат. Примитивный каркас для фильтра будет состоять из определениямножества операций, которые должен реализовать прикладной программист:class filter {public:class Retry {public:virtual const char* message() { return 0; }};virtualvirtualvirtualvirtualvirtualvirtualvoid start() { }int retry() { return 2; }int read() = 0;void write() { }void compute() { }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;}}}Теперь прикладную программу можно написать так:354Бьерн Страуструп.Язык программирования С++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 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* в более удобноесемейство надежных классов, представляющих списки. Чтобы применение интерфейсных классов небыло слишком накладно, нужно использовать функции-подстановки.
В примерах, подобныхприведенному, где задача функций-подстановок только подогнать тип, накладные расходы в памяти искорости выполнения программы не возникают.Естественно, можно считать интерфейсным абстрактный базовый класс, который представляет355Бьерн Страуструп.Язык программирования С++абстрактный тип, реализуемый конкретными типами ($$13.3), также как и управляющие классы израздела 13.9. Но здесь мы рассматриваем классы, у которых нет иных назначений - только задачаадаптации интерфейса.Рассмотрим задачу слияния двух иерархий классов с помощью множественного наследования.