Обширный интерфейс
Когда обсуждались абстрактные типы ($$13.3) и узловые классы ($$13.4),было подчеркнуто, что все функции базового класса реализуютсяв самом базовом или в производном классе. Но существует и другойспособ построения классов. Рассмотрим, например, списки, массивы,ассоциативные массивы, деревья и т.д. Естественно желание для всехэтих типов, часто называемых контейнерами, создать обобщающий ихкласс, который можно использовать в качестве интерфейса с любымиз перечисленных типов. Очевидно, что пользователь не должензнать детали, касающиеся конкретного контейнера. Но задачаопределения интерфейса для обобщенного контейнера нетривиальна.Предположим, что такой контейнер будет определен как абстрактныйтип, тогда какие операции он должен предоставлять? Можно предоставитьтолько те операции, которые есть в каждом контейнере, т.е.пересечение множеств операций, но такой интерфейс будет слишкомузким. На самом деле, во многих, имеющих смысл случаях такоепересечение пусто. В качестве альтернативного решения можнопредоставить объединение всех множеств операций и предусмотретьдинамическую ошибку, когда в этом интерфейсе к объектуприменяется "несуществующая" операция. Объединение интерфейсов классов,представляющих множество понятий, называется обширным интерфейсом.Опишем "общий" контейнер объектов типа 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; //... } void user(container& c1, container& c2) { T* p1 = c1.get(); T* 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 { // Приехали! // А что теперь делать? } } Оба способа обнаружения ошибки, показанные на этих примерах,приводят к программе с "раздутым" кодом и низкой скоростьювыполнения. Поэтому обычно просто игнорируют возможные ошибкив надежде, что пользователь на них не натолкнется. Но задача от этогоне упрощается, ведь полное тестирование затруднительно и требуетмногих усилий. Поэтому, если целью является программа с хорошими характеристиками,или требуются высокие гарантии корректности программы, или, вообще,есть хорошая альтернатива, лучше не использовать обширные интерфейсы.Кроме того, использование обширного интерфейса нарушаетвзаимнооднозначное соответствие между классами и понятиями, и тогданачинают вводить новые производные классы просто для удобствареализации.