Бьерн Страуструп (947334), страница 76
Текст из файла (страница 76)
набором операций. Имеется взаимнооднозначное соответствие между
интерфейсом класса и его реализацией. Ни один из них (изначально)
не предназначался в качестве базового для получения производных классов.
Обычно в иерархии классов конкретные типы стоят особняком. Каждый
конкретный тип можно понять изолированно, вне связи с другими классами.
Если реализация конкретного типа удачна, то работающие с ним программы
сравнимы по размеру и скорости со сделанными вручную программами,
в которых используется некоторая специальная версия общего понятия.
Далее, если произошло значительное изменение реализации, обычно
модифицируется и интерфейс, чтобы отразить эти изменения. Интерфейс,
по своей сути, обязан показать какие изменения оказались существенными
в данном контексте. Интерфейс более высокого уровня оставляет
больше свободы для изменения реализации, но может ухудшить
характеристики программы. Более того, хорошая реализация зависит
только от минимального числа действительно существенных классов.
Любой из этих классов можно использовать без накладных расходов,
возникающих на этапе трансляции или выполнения, и вызванных
приспособлением к другим, "сходным" классам программы.
Подводя итог, можно указать такие условия, которым должен
удовлетворять конкретный тип:
[1] полностью отражать данное понятие и метод его реализации;
[2] с помощью подстановок и операций, полностью использующих
полезные свойства понятия и его реализации, обеспечивать
эффективность по скорости и памяти, сравнимую
с "ручными программами";
[3] иметь минимальную зависимость от других классов;
[4] быть понятным и полезным даже изолированно.
Все это должно привести к тесной связи между пользователем и
программой, реализующей конкретный тип. Если в реализации произошли
изменения, программу пользователя придется перетранслировать,
поскольку в ней наверняка содержатся вызовы функций, реализуемые
подстановкой, а также локальные переменные конкретного типа.
Для некоторых областей приложения конкретные типы обеспечивают
основные типы, прямо не представленные в С++, например:
комплексные числа, вектора, списки, матрицы, даты, ассоциативные
массивы, строки символов и символы, из другого (не английского)
алфавита. В мире, состоящем из конкретных понятий, на самом деле
нет такой вещи как список. Вместо этого есть множество списочных
классов, каждый из которых специализируется на представлении
какой-то версии понятия список. Существует дюжина списочных
классов, в том числе: список с односторонней связью; список с
двусторонней связью; список с односторонней связью, в котором
поле связи не принадлежит объекту; список с двусторонней связью,
в котором поля связи не принадлежат объекту; список с односторонней
связью, для которого можно просто и эффективно определить входит
ли в него данный объект; список с двусторонней связью, для
которого можно просто и эффективно определить входит ли в него данный
объект и т.д.
Название "конкретный тип" (CDT - concrete data type, т.е.
конкретный тип данных) , было выбрано по контрасту с термином
"абстрактный тип" (ADT - abstract data type, т.е. абстрактный тип
данных). Отношения между CDT и ADT обсуждаются в $$13.3.
Существенно, что конкретные типы не предназначены для явного
выражения некоторой общности. Так, типы slist и vector можно
использовать в качестве альтернативной реализации понятия
множества, но в языке это явно не отражается. Поэтому, если
программист хочет работать с множеством, использует конкретные
типы и не имеет определения класса множество, то он должен выбирать
между типами slist и vector. Тогда программа записывается в
терминах выбранного класса, скажем, slist, и если потом предпочтут
использовать другой класс, программу придется переписывать.
Это потенциальное неудобство компенсируется наличием всех
"естественных" для данного класса операций, например таких, как
индексация для массива и удаление элемента для списка. Эти
операции представлены в оптимальном варианте, без "неестественных"
операций типа индексации списка или удаления массива, что могло
бы вызвать путаницу. Приведем пример:
void my(slist& sl)
{
for (T* p = sl.first(); p; p = sl.next())
{
// мой код
}
// ...
}
void your(vector& v)
{
for (int i = 0; i<v.size(); i++)
{
// ваш код
}
// ...
}
Существование таких "естественных" для выбранного метода реализации
операций обеспечивает эффективность программы и значительно облегчает
ее написание. К тому же, хотя реализация вызова подстановкой обычно
возможна только для простых операций типа индексации массива или
получения следующего элемента списка, она оказывает значительный
эффект на скорость выполнения программы. Загвоздка здесь состоит в том,
что фрагменты программы, использующие по своей сути эквивалентные операции,
как, например, два приведенных выше цикла, могут выглядеть непохожими
друг на друга, а фрагменты программы, в которых для эквивалентных
операций используются разные конкретные типы, не могу заменять друг
друга. Обычно, вообще, невозможно свести сходные фрагменты программы
в один.
Пользователь, обращающийся к некоторой функции, должен точно
указать тип объекта, с которым работает функция, например:
void user()
{
slist sl;
vector v(100);
my(sl);
your(v);
my(v); // ошибка: несоответствие типа
your(sl); // ошибка: несоответствие типа
}
Чтобы компенсировать жесткость этого требования, разработчик некоторой
полезной функции должен предоставить несколько ее версий, чтобы у
пользователя был выбор:
void my(slist&);
void my(vector&);
void your(slist&);
void your(vector&);
void user()
{
slist sl;
vector v(100);
my(sl);
your(v);
my(v); // теперь нормально: вызов my(vector&)
your(sl); // теперь нормально: вызов your(slist&)
}
Поскольку тело функции существенно зависит от типа ее параметра,
надо написать каждую версию функций my() и your() независимо друг
от друга, что может быть хлопотно.
С учетом всего изложенного конкретный тип, можно сказать, походит
на встроенные типы. Положительной стороной этого является тесная
связь между пользователем типа и его создателем, а также между
пользователями, которые создают объекты данного типа, и пользователями,
которые пишут функции, работающие с этими объектами. Чтобы
правильно использовать конкретный тип, пользователь должен
разбираться в нем детально. Обычно не существует каких-то
универсальных свойств, которыми обладали бы все конкретные типы
библиотеки, и что позволило бы пользователю, рассчитывая на эти
свойства, не тратить силы на изучение отдельных классов. Такова
плата за компактность программы и эффективность ее выполнения.
Иногда это вполне разумная плата, иногда нет. Кроме того, возможен
такой случай, когда отдельный конкретный класс проще понять и
использовать, чем более общий (абстрактный) класс. Именно так
бывает с классами, представляющими хорошо известные типы данных,
такие как массивы или списки.
Тем не менее, укажем, что в идеале надо скрывать, насколько
возможно, детали реализации, пока это не ухудшает характеристики
программы. Большую помощь здесь оказывают функции-подстановки.
Если сделать открытыми переменные, являющиеся членами, с помощью описания
public, или непосредственно работать с ними с помощью функций, которые
устанавливают и получают значения этих переменных, то почти всегда
это приводит к плохому результату. Конкретные типы должны быть все-таки
настоящими типами, а не просто программной кучей с нескольким функциями,
добавленными ради удобства.
13.3 Абстрактные типы
Самый простой способ ослабить связь между пользователем класса
и его создателем, а также между программами, в которых объекты
создаются, и программами, в которых они используются, состоит в введении
понятия абстрактных базовых классов. Эти классы представляют
интерфейс со множеством реализаций одного понятия. Рассмотрим
класс set, содержащий множество объектов типа T:
class set {
public:
virtual void insert(T*) = 0;
virtual void remove(T*) = 0;
virtual int is_member(T*) = 0;
virtual T* first() = 0;
virtual T* next() = 0;
virtual ~set() { }
};
Этот класс определяет интерфейс с произвольным множеством (set),
опираясь на встроенное понятие итерации по элементам множества.
Здесь типично отсутствие конструктора и наличие виртуального
деструктора, см. также $$6.7. Рассмотрим пример:
class slist_set : public set, private slist {
slink* current_elem;
public:
void insert(T*);
void remove(T*);
int is_member(T*);
virtual T* first();
virtual T* next();
slist_set() : slist(), current_elem(0) { }
};
class vector_set : public set, private vector {
int current_index;
public:
void insert(T*);
void remove(T*);
int is_member(T*);
T* first() { current_index = 0; return next(); }
T* next();
vector_set(int initial_size)
: array(initial_size), current_index(0) { }
};
Реализация конкретного типа используется как частный базовый
класс, а не член класса. Это сделано и для удобства записи, и потому,
что некоторые конкретные типы могут иметь защищенный интерфейс
с целью предоставить более прямой доступ к своим членам из производных
классов. Кроме того, подобным образом в реализации могут использоваться
некоторые классы, которые имеют виртуальные функции и не являются
конкретными типами. Только с помощью образования производных классов
можно в новом классе изящно переопределить (подавить) виртуальную
функцию класса реализации. Интерфейс определяется абстрактным классом.
Теперь пользователь может записать свои функции из $$13.2
таким образом:
void my(set& s)
{
for (T* p = s.first(); p; p = s.next())
{
// мой код
}
// ...
}
void your(set& s)
{
for (T* p = s.first(); p; p = s.next())
{
// ваш код
}
// ...
}
Стало очевидным сходство между двумя функциями, и теперь достаточно
иметь только одну версию для каждой из функций my() или your(),
поскольку для общения с slist_set и vector_set обе версии используют
интерфейс, определяемый классом set:
void user()
{
slist_set sl;
vector_set v(100);
my(sl);
your(v);
my(v);
your(sl);
}
Более того, создатели функций my() и your() не обязаны знать описаний
классов slist_set и vector_set, и функции my() и your() никоим
образом не зависят от этих описаний. Их не надо перетранслировать
или как-то изменять, ни если изменились классы slist_set или
vector_set ни даже, если предложена новая реализация этих классов.
Изменения отражаются лишь на функциях, которые непосредственно
используют эти классы, допустим vector_set. В частности, можно
воспользоваться традиционным применением заголовочных файлов и
включить в программы с функциями my() или your() файл определений
set.h, а не файлы slist_set.h или vector_set.h.
В обычной ситуации операции абстрактного класса задаются как
чистые виртуальные функции, и такой класс не имеет членов,
представляющих данные (не считая скрытого указателя на таблицу
виртуальных функций). Это объясняется тем, что добавление невиртуальной
функции или члена, представляющего данные, потребует определенных
допущений о классе, которые будут ограничивать возможные реализации.
Изложенный здесь подход к абстрактным классам близок по духу традиционным
методам, основанным на строгом разделении интерфейса и его реализаций.
Абстрактный тип служит в качестве интерфейса, а конкретные типы
представляют его реализации.
Такое разделение интерфейса и его реализаций предполагает
недоступность операций, являющихся "естественными" для какой-то