Б. Страуструп - Язык программирования С++. Специальное издание, 3-изд. Бином. 2004 (1160791), страница 192
Текст из файла (страница 192)
Чтобы сделать альтернативу шаблон/иерархия более конкретной, рассмотрим, как обобщить простую итерацию: эо!с(рп'п! аП (Пег аког Тх! ( 1ог (Т р=к!т гэ! (), р, р = х и ех1 ()) сои1 « *р, ) Здесь предполагается, что итератор 11ег )ог 1 обеспечивает операции, дающие Т". Мы можем сделать итератор11ег ~ог Тпараметром шаблона: 1егпр!а1есс!асс Пег 1ог Т иоЫрг1п! аП (Пег1ог Тх) ( (ог (Т'р=х1шэ! ();р;р =х псх1(()) сои1 «'р; Это позволяет нам использовать разнообразные не связанные между собой итераторы, если все они предоставляют Т!гэ1 () и пех1 () с правильным смыслом, и если во время компиляции мы знаем тнп итератора для каждого вызова рг!п1 ад ().
На этой идее основываются контейнеры и алгоритмы стандартной библиотеки. С другой стороны, мы можем воспользоваться наблюдением, что Т!гв1 () и пех1 () образуют интерфейс для итераторов. Тогла для представления этого интерфейса мы можем определить класс: с!асс 11ег ( риЬПа о!ггиа! Т*~псг () сопс1 = О; о!г(иа! Т" пех1 () = 0; ооЫ рг!п1 аП2 (11сгЬ х) ( 1ог (Т* р =- х1! гс1 (); р, р = х и ех1 ()) сои1 « 'р; Теперь мы можем пользоваться любыми итераторами, производными от 11ег. Используем ли мы для представления параметрнзации шаблоны или иерархию классов, зто не влияет на действительный код — отличаются только быстродействие, скорость повторной компиляции и т.
п. В частности, класс 11ег является кандидатом на использование в качестве аргумента шаблона: 835 24.4. Компоненты иоиЦ(!гегй й ( рпп! а!1 (1(; О использование шаблона рппг а112 (1(; Следовательно, эти два подхода мы можем рассматривать как дополняющие друг друга. Часто шаблону как частью своей реализации нужно пользоваться функциями и классами. Многие из ннх для сохранения универсальности и эффективности сами должны быть шаблонами.
Б этом смысле алгоритмы становятся обобщенными по множеству типов. Этот стиль использования шаблонов называется обобщенньгн программированием Я 2.7). Когда мы вызываем яЫсюг1 () для вектора, элементы вектора служат операндами сортировки юг! (); таким образом, алгоритм воя!является обобщенным по типу сортируемых элементов. Кроме того, стандартная сортировка является обобщенной по типам контейнеров, поскольку к ней можно обрагцаться для произвольных контейнеров, удовлетворяющих стандартам Я 16.3.1), Ллгоритм яог! (( также параметризуется критерием сравнения (ч 187.1). С точки зрения проектирования, это отличается от принятия операции в качестве аргумента и от обобщения алгоритма по типу операнда. Решение параметризовать алгоритм для объекта (или операции) так, чтобы управлять действиями алгоритма, принадлежит гораздо более высокому уровню проектировшгия. Это решение дает проектировщику/программисту возможность влиять на политику выполнения определенных действий в алгоритме.
Однако с точки зрения языка программирования здесь нет никакой разницы. 24.4.2. Интерфейсы и реализации Идеальный интерфейс представляет пользователю полньш и согласованный набор понятии; совместим по всем частям компоненты; не открывает пользователю подробностей реализации; может быть реализован несколькими способами; статически типизируется; выражается при помощи типов прикладного уровня; ограничен и строго определенным образом зависит от других интерфейсов. Отметив необходимость согласованности по классам, представляющим для внешнего мира интерфейс компоненты Я 24А), давайте забудем об этом и упростим обсуждение, рассматривая лишь один класс.
с1аяя У( /" ... */); 0 нужен для Х с!аяя 2( /' ... */); //нужен дляХ е!аяя Х ( // пример плохого иншерреп!еа Уа; 20; раббе: оо!е(,/(сопя(сйаг' .. (; ооЫд(1пг((, ьа1(; ооЫяег а(УЦ У! Хег а((; 836 Глава 24. Проектирование и программирование Этот интерфейс заключает в себе несколько потенциальных проблем; ° Он пользуется типами У и Х так, что для компиляции требуется знать их обьявления.
Функция Х:у"принимает произвольное число аргументов неизвестного типа (вероятно под управлением некоторой «форматирующей строки», заданной в качестве первого аргумента; 8 21.8). Функция Хсд принимает аргумент 1п1Ц. Это может быть допустимо, но обычно является признакам того, что уровень абстракции слишком низок. Массив целых чисел не описывает сам себя, поэтому неочевидно, сколько элементов он в себе содержит. Функции зе1 а () и де1 а (~ с болыцой вероятностью открывают представление объектов класса Х, позволяя прямой доступ к Хса. Эти функпии-члены предоставляют интерфейс на очень низком уровне абстракции.
По сути дела классы с интерфейсами такого уровня принадлежат к деталям большей компоненты — если они вообще чему-либо принадлежат. В идеале аргумент интерфейсной функции несет в себе достаточно информации, чтобы описать самого себя. Эмпирическое правило таково: должна быть возможность передать удаленному серверу как можно более краткий запрос на обслуживание. С++ позволяет программисту выставить представление класса наружу как часть интерфейса.
Это представление может быть спрятано (при помощи рг1оа1е или рго1ес1ес(), но оно доступно компилятору, чтобы распределить память под автоматические переменные, позволить встраивание функций и т. д. Отрицательный эффект от этого состоит в том, что использование классов-типов в представлении класса может внести нежелательные взаимозависимости. Является ли проблемой использование членов типа Уи Х, зависит от того, какими типами Уи Х являются на самом деле. Вели это простые типы, такие как 11е1, сотр1ех и е1г1пд, их использование как правило вполне оправдано. Такие типы можно считать стабильными, и необходимость включать их в объявления классов — приемлемая нагрузка для компилятора.
Однако если бы Уи Х сами были интерфейсными классами важных компонент, таких как графическая система нли система управления банковскими расчетами, было бы разумно не зависеть от них «слишком непосредственною В подобных случаях лучшим решением является использование члена-указателя илн ссылки; с1аее У; с1азз Х; с1аззХ( 1/Хитеет с~осо1уи к Уи Хтол»ко через указатели и ссылки У' а; ХЬ6; 0- ); Это разъединяет определение Хи определения У и Х; то есть определение Х зависит только от имен Уи У.
Реализация Хбудет, конечно же, по-прежнему зависеть от определений Уи Х, но это не будет вредить пользователям Х. Это иллюстрирует важную мысль: интерфейс, скрывающий значительную часть информации, — как и должен делать хороший интерфейс — будет иметь гораздо меньше зависимостей, чем скрываемая им реализация. Например, определение класса Х 837 24.4. Компоненты можно откомпилировать без доступа к определениям Уи У. Однако определения функций-членов классаХ манипулирующих объектами Уи У будут нуждаться в доступе к определениям Уи У. Когла анализируются зависимости, зависимости интерфейса и реализации должны рассматриваться раздельно. В обоих случаях, чтобы облегчить понимание и тестирование системы, было бы идеально, если бы графы зависимости системы были направленными ациклическими графами.
Однако этот идеал гораздо более важен и гораздо чаще достижим для интерфейсов, чем лля реализаций. Отметим, что класс может определять три интерфейса: с(авв Х( ргсиаГе: //закрытый // досчлупны только для членов и друзей // за и1 и щ вин ай // доступны только для членов и друзей, //а также членов и друзей производиык классов // открытии //доступны для широкой публики рго1есгед: риойс. Кроме того, друзья/г(епс( являются'частью открытого интерфейса Ц 11.5). Член должен быть частшо самого ограничительного интерфейса.
То есть, член должен быть закрытым, если нет причины сделать его более доступным. Если он все же должен бьшь более доступным, его нужно сделать защищенным. если только пет причины его открывать. Практически во всех случаях делать члены данных открытыми или защищенными очень плохо. Не члены данных, а функции и классы, составляющие открытый интерфейс, должны представлять взгляд на класс, согласующийся с его ролью, как выразителя некоторой концепции. Отметим, что лля следующего уровня сокрытия представлении можно использовать абстрактные классы 8 2.5А, 8 12,3, 8 25.3). 24.4.3. Жирные интерфейсы В идеале интерфейс должен предлагать только те операции, которые имеют смысл и могут быть хорошо реализованы каждым производным классом, реализующим этот интерфейс.
Однако это пе всегда легко. Рассмотрим списки, массивы, ассоциативные массивы, деревья и т. и. Как показано в 8 16.2.2, соблазнительно и иногда полезно обобщить все эти типы в один — обычно называемый контейнером — который можно было бы использовать в качестве интерфейса ко всем ним. Это (казалось бы) избавит пользователя от необходимости иметь дело с деталяльи всех этих контейнеров. Однако определить интерфейс универсального контейнерного класса не так просто. Допустим, мы хотим определить Соп1аспег как абстрактный тип.