Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 192
Текст из файла (страница 192)
Еше раз отметим, что не все элементы компонента являются классами. В идеале компонент должен описываться набором интерфейсов, которые он использует для собственной реализации, и набором интерфейсов, которые он предоставляет своим пользователям. Все прочее — это внутренние детали реализации, которые должны скрываться от «внешнего мира».
Набор интерфейсов — это термины, в которых компонент описывает его проектировшик. Программист же вынужден отображать интерфейсы в объявления. Классы и иерархии классов определяют интерфейсы, а пространства имен позволяют сгруппировать их в наборы, которые компонент использует сам и которые он предоставляет пользователям: Используемое реализацией Х Используемое интерфейсом Х ( И,фю.х ) Реализацю~ Х 24 4.
Компоненты 888 При использовании техники, описанной в 58.2.4. ), это превращается в следующее: патезрасе А ( У ... // интерфейс компонента Х //зависит от обьявлений иэ А патезрасе Х из!ид пагиезрасе А; гойЦ'( ); У средства, нужные реализации Х иаеезрасе Х 1тр1 ( из!ид иаеезрасе Х; // ... ) го!а Х::!') ) из)ид паеезрасе Х 1ер1; //... //зависит от обьявлений из Х 1тр! иагиезрасе Х ипр1 с1ат ИИде! ( // ... // ... Это гарантирует, что ИИяе! не может использоваться из других частей программы. Однако классы, которые отражают общеполезные понятия„являются хорошими кандидатами для многократного повторного использования, и их желательно включать в интерфейс компонента: с1азз Саг ) с1азз 6'йее! ( // ..
)' ИЧюее!Яы, !ггг, г1и, ггзг; Основной интерфейс Х компонента не должен зависеть от интерфейса реализации Х!ер1 Компонент может содержать множество классов, не предназначенных для общего использования. Такие классы следует прятать внутри классов реализации или пространств имен: 886 Глава 24. Проектирование и программирование риЫ1с: ~У... ); Во многих случаях колеса лучше спрятать ради чистоты абстрактной концепции автомобиля (нельзя использовать колеса в отрыве от главного понятия — автомобиля).
Однако класс колес И%ее! сам по себе кажется вполне пригодным для самостоятельного использования, так что возможно его все же лучше вынести из класса Саг: с(азз 1г'л ее! ( 11 ... )' с1азз Саг ( )гяее!Яе, уги, гыч ггв; Ф ... риЫ(с: У ... ); Решение оставить класс внутри или вынести наружу (оба способа широко применяются на практике) зависит от конкретных целей проекта и совокупности понятий, составляющих проект. По умолчанию класс локализуют как только могут до тех пор, пока не появляются серьезные причины сделать его более доступным. У интересных функций и данных есть отвратительная тенденция всплывать в глобальное пространство имен, в широко используемые пространства имен илн корневой класс иерархии.
Это может ненароком приоткрыть детали реализации и привести к проблемам, характерным для глобальных функций н данных. Вероятность этого увеличивается в случае однокоренных иерархий и для проектов с малым числом пространств имен. В контексте классовых иерархий с этим можно бороться с помощью виртуальных базовых классов (915.2.4). Проблема же малого числа пространств имен решается введением множества небольших реализационных пространств имен. Заметьте, что заголовочные файлы обеспечивают мощный механизм предоставления разных видов на один и тот же компонент для разных групп пользователей, а также для устранения пользовательской информации о классах реализации (в9.3.2). 24.4.1.
Шаблоны С проектной точки зрения шаблоны служат двум, слабо связанным между собой целям: ° Обобщенному программированию. ° Политике параметризации. На ранних стадиях проектирования операции — это просто операции. Но на более поздних стадиях, когда появляется необходимость описать типы их операндов, проявляется особая важность шаблонов для таких статически типизированных язы- 24.4. Компоненты 887 ков программирования, как С++.
Без шаблонов стали бы повторяться определения функций, а иначе проверка типов была бы отнесена на стадию выполнения программ (524.2.3). Наилучшим кандидатом на роль шаблонов являются операции, реализующие универсальные алгоритмы, пригодные для разных типов. Если все такие операнды относятся к одной иерархии классов, и особенно если нужно добавлять типы операндов на стадии выполнения программы, тогда тип операнда лучше всего представить классом (часто абстрактным классом). Если же типы операндов не вписываются в единственную иерархию классов и если особо важна эффективность, операцию лучше всего реализовать в виде шаблона. Стандартные контейнеры и поддерживающие их алгоритмы иллюстрируют ситуацию, когда несвязанные между собой типы операндов и требование высокой эффективности вынуждают использовать шаблоны (516.2).
Чтобы предметнее проиллюстрировать альтернативу шаблоны/иерархия рассмотрим, как можно обобщить простую итерацию: гоЫрпп(аИ(11ег 2ог Тх) (ог ( Т* р = х. рг(1 () ( р( р = х. пех(() ) сои( «*р; ) Здесь предполагается, что йег ~ог Тобеспечивает операции, возвращающие Т'. Мы можем сделать итератор йег 1ог Тпараметром шаблона: (етр(а(с<с(ат 1(ег 1ог Т> гоЫ рпп( аИ (йег Тог Т х) ( То( (т' р = х.Игл(() ( р( р = х.
псх(() ) сои( «р; Это позволяет нам использовать самые разные итераторы до тех пор, пока они предоставляют операции1(гм () и лех( () надлежащего смысла, и если во время компиляции мы знаем типы итераторов для каждого вызова рг(л( аИ() . Стандартные контейнеры и алгоритмы базируются на этой идее. С другой стороны, мы можем трактоватьЯгл(() и псх(() как интерфейс к итера- торам, и определить класс, представляющий этот интерфейс: с!ааа 1(ег ( риЫ(с: Ыг(иа! Т* Т(гл( ( ) соли = О; г(г(иа! Т* лсх1 () = О; )( го((( рг(л( аИ2 (1(еге х) ( 1ог(Т* р = х.((г(1(); р; р = х.псх(() ) сои( « "р; ) Мы можем пользоваться любыми итераторами, производными от йег. Как видно, реальный клиентский код не зависит от того, используем ли мы для аргументов параметры шаблона или классовую иерархию — различаются лишь эффективность и нюансы, связанные с перекомпиляцией.
В частности, вот конкретный пример использования класса йег в качестве аргумента шаблона: 888 Глава 24 Проектирование и программирование иоЫ2'(21ега т) ( рг(нг ап (1); рг(пг а112 (1); ) У используется шаблон 24.4.2. Интерфейсы и реализации Идеальный интерфейс: ° предоставляет пользователю полный и согласованный набор концепций, ° согласован для всех частей компонента, ° не открывает деталей реализации пользователям, ° может реализовываться разными способами, ° статически типизирован, ° выражается с помощью типов уровня приложения, ° зависит ограниченно и строго определенным образом от других интерфейсов. Отметив необходимость согласованности классов, представляющих интерфейс компонента остальному миру (524.4), мы можем теперь упростить изложение, ограничившись единственным классом: с!азз У (l*...
*l ); сглаз с (l* ° ° ° *I) ' ~У нужен для Х ~У нужен для Х с1аю Х ( уа; ХЬ) У пример плохого стиля интерфейса риЫ1с: иоз)1 г (сопят сйаг* ... ) Следовательно, два рассматриваемых подхода могут и дополнять друг друга. Часто шаблону требуются функции и классы в качестве части своей реализации. Многие из них для сохранения универсальности и эффективности сами должны быть шаблонами. В этом смысле алгоритмы становятся обобщенными по множеству типов.
Такой стиль применения шаблонов называется обобщенным программированием (8епезтс ргобгатт(п8) (82.7). Когда мы вызываем ю1: гааге() для контейнера гесгог, элементы вектора становятся операндами операции хогг(); можно сказать, что алгоритм зогг() является обобщенным по типу элементов вектора. Кроме того, этот алгоритм является обобщенным и по типам контейнеров, поскольку задействуются лишь итераторы произвольных стандартных контейнеров (516.3.1). Еше алгоритм хогг() параметризован по критерию сравнения 618.7.1).
С точки зрения проектировщика это отличается от простой параметризации операции по типам ее аргументов. Решение параметризовать алгоритм таким образом, чтобы влиять на особенности работы алгоритма, является проектным решением более высокого уровня. Тем самым проектировщик/программист получают дополнительный канал влияния на определение политики выполнения некоторых действий в алгоритме.
Однако с точки зрения языка программирования никаких особенностей тут нет. 889 24.4. Компоненты гоЫ е(Ы( [), 1иг); иоЫлез а(уь); Уь яег а(); У этого интерфейса есть несколько потенциальных проблем: ° Он использует типы Х и У'таким образом, что для компиляции требуется знать их объявления.
° Функция Х: у() принимает произвольное число аргументов неизвестного типа (возможно, под управлением «форматирующей строки», передаваемой в качестве первого аргумента; 521.8). ° Функция Х::я() принимает аргумент типа 1лг[]. Это допустимо, но означает низкий уровень абстракции. Массив целых чисел не описывает себя сам, и неясно, сколько элементов он содержит. ° Функции зег а() и я»Ч а() с большой вероятностью открывают детали представления класса Х разрешая прямой доступ к Х:: а.