Компоненты
|
|
Интерфейсы и реализации
|
Свод правил
В этой главе мы коснулись многих тем, но, как правило, избегалидавать настоятельные и конкретные рекомендации по рассматриваемымвопросам. Это отвечает моему убеждению, что нет "единственно верногорешения". Принципы и приемы следует применять способом, наиболееподходящим для конкретной задачи. Здесь требуются вкус, опыт иразум. Тем не менее, можно предложить свод правил, которыеразработчик может использовать в качестве ориентиров, пока неприобретет достаточно опыта, чтобы выработать лучшие.Этот свод правил приводится ниже. Он может служить отправной точкой в процессе выработкиосновных направлений проекта конкретной задачи, или же он можетиспользоваться организацией в качестве проверочного списка. Подчеркнуеще раз, что эти правила не являются универсальными и не могутзаменить собой размышления. - Нацеливайте пользователя на применение абстракции данных и объектно-ориентированного программирования. - Постепенно переходите на новые методы, не спешите. - Используйте возможности С++ и методы обЪектно-ориентированного программирования только по мере надобности. _ Добейтесь соответствия стиля проекта и программы. - Концентрируйте внимание на проектировании компонента. _ Используйте классы для представления понятий. - Используйте общее наследование для представления отношений "есть". - Используйте принадлежность для представления отношений "имеет". - Убедитесь, что отношения использования понятны, не образуют циклов, и что число их минимально. - Активно ищите общность среди понятий области приложения и реализации, и возникающие в результате более общие понятия представляйте как базовые классы. - Определяйте интерфейс так, чтобы открывать минимальное количество требуемой информации: - Используйте, всюду где это можно, частные данные и функции-члены. - Используйте описания public или protected, чтобы отличить запросы разработчика производных классов от запросов обычных пользователей. - Сведите к минимуму зависимости одного интерфейса от других. - Поддерживайте строгую типизацию интерфейсов. - Задавайте интерфейсы в терминах типов из области приложения.Дополнительные правила можно найти $$11.5.* ПРОЕКТИРОВАНИЕ БИБЛИОТЕК
Проект библиотеки - это проект языка, (фольклор фирмы Bell Laboratories)... и наоборот. - А. Кениг Эта глава содержит описание различных приемов, оказавшихся полезнымипри создании библиотек для языка С++. В частности, в нейрассматриваются конкретные типы, абстрактные типы, узловые классы,управляющие классы и интерфейсные классы. Помимо этого обсуждаютсяпонятия обширного интерфейса и структуры области приложения,использование динамической информации о типах и методы управленияпамятью. Внимание акцентируется на том, какими свойствами должныобладать библиотечные классы, а не на специфике языковых средств,которые используются для реализации таких классов, и не наопределенных полезных функциях, которые должна предоставлять библиотека.Введение
Разработка библиотеки общего назначения - это гораздо более труднаязадача, чем создание обычной программы. Программа - это решениеконкретной задачи для конкретной области приложения, тогда какбиблиотека должна предоставлять возможность решение для множества задач,связанных с многими областями приложения. В обычной программепозволительны сильные допущения об ее окружении, тогда как хорошуюбиблиотеку можно успешно использовать в разнообразных окружениях,создаваемых множеством различных программ. Чем более общей и полезнойокажется библиотека, тем в большем числе окружений она будетпроверяться, и тем жестче будут требования к ее корректности, гибкости,эффективности, расширяемости, переносимости, непротиворечивости,простоте, полноте, легкости использования и т.д. Все же библиотекане может дать вам все, поэтому нужен определенный компромисс.Библиотеку можно рассматривать как специальный, интересный варианттого, что в предыдущей главе мы называли компонентом. Каждыйсовет по проектированию и сопровождению компонентов становитсяпредельно важным для библиотек, и, наоборот, многие методыпостроения библиотек находят применение при проектировании различныхкомпонентов. Было бы слишком самонадеянно указывать как следуетконструировать библиотеки. В прошлом оказались успешными несколькоразличных методов, а сам предмет остается полем активных дискуссийи экспериментов. Здесь только обсуждаются некоторые важные аспектыэтой задачи и предлагаются некоторые приемы, оказавшиеся полезнымипри создании библиотек. Не следует забывать, что библиотеки предназначеныдля совершенно разных областей программирования, поэтому не приходитсярассчитывать, что какой-то один метод окажется наиболее приемлемым длявсех библиотек. Действительно, нет никаких причин полагать, что методы,оказавшиеся полезными при реализации средств параллельногопрограммирования для ядра многопроцессорной операционной системы,окажутся наиболее приемлемыми при создании библиотеки, предназначеннойдля решения научных задач, или библиотеки, представляющей графическийинтерфейс. Понятие класса С++ может использоваться самыми разнымиспособами, поэтому разнообразие стилей программирования можетпривести к беспорядку. Хорошая библиотека для сведения такогобеспорядка к минимуму обеспечивает согласованный стиль программирования,или, по крайней мере, несколько таких стилей. Этот подход делаетбиблиотеку более "предсказуемой", а значит позволяет легче и быстрееизучить ее и правильно использовать. Далее описываются пять"архитипичных" классов, и обсуждаются присущие им сильные и слабыестороны: конкретные типы ($$13.2), абстрактные типы ($$13.3),узловые классы ($$13.4), интерфейсные классы ($$13.8), управляющиеклассы ($$13.9). Все эти виды классов относятся к области понятий,а не являются конструкциями языка. Каждое понятие воплощаетсяс помощью основной конструкции - класса. В идеале надо иметьминимальный набор простых и ортогональных видов классов, исходя изкоторого можно построить любой полезный и разумно-определенный класс.Идеал нами не достигнут и, возможно, недостижим вообще. Важно понять,что любой из перечисленных видов классов играет свою роль припроектировании библиотеки и, если рассчитывать на общее применение,никакой из них не является по своей сути лучше других. В этой главе вводится понятие обширного интерфейса ($$13.6),чтобы выделить некоторый общий случай всех этих видов классов.С помощью него определяется понятие каркаса области приложения ($$13.7). Здесь рассматриваются прежде всего классы, относящиеся строго кодному из перечисленных видов, хотя, конечно, используютсяклассы и гибридного вида. Но использование класса гибридного видадолжно быть результатом осознанного решения, возникшего при оценкеплюсов и минусов различных видов, а не результатом пагубного стремленияуклониться от выбора вида класса (слишком часто "отложим пока выбор"означает просто нежелание думать). Неискушенным разработчикамбиблиотеки лучше всего держаться подальше от классов гибридноговида. Им можно посоветовать следовать стилю программирования той изсуществующих библиотек, которая обладает возможностями, необходимыми дляпроектируемой библиотеки. Отважиться на создание библиотеки общегоназначения может только искушенный программист, и каждый создательбиблиотеки впоследствии будет "осужден" на долгие годы использования,документирования и сопровождения своего собственного создания. В языке С++ используются статические типы. Однако, иногдавозникает необходимость в дополнение к возможностям, непосредственнопредоставляемым виртуальными функциями, получать динамическую информациюо типах. Как это сделать, описано в $$13.5. Наконец, перед всякойнетривиальной библиотекой встает задача управления памятью. Приемы еерешения рассматриваются в $$13.10. Естественно, в этой главе невозможнорассмотреть все методы, оказавшиеся полезными при создании библиотеки.Поэтому можно отослать к другим местам книги, где рассмотреныследующие вопросы: работа с ошибками и устойчивость к ошибкам ($$9.8),использование функциональных объектов и обратных вызовов ($$10.4.2и $$9.4.3), использование шаблонов типа для построения классов($$8.4). Многие темы этой главы связаны с классами, являющимися контейнерами,(например, массивы и списки). Конечно, такие контейнерные классыявляются шаблонами типа (как было сказано в $$1.и 4.3 $$8). Ноздесь для упрощения изложения в примерах используются классы,содержащие указатели на объекты типа класс. Чтобы получить настоящуюпрограмму, надо использовать шаблоны типа, как показано в главе 8.Конкретные типы
Такие классы как vector ($$1.4), Slist ($$8.3), date ($$5.2.2) иcomplex ($$7.3) являются конкретными в том смысле, что каждый изних представляет довольно простое понятие и обладает необходимымнабором операций. Имеется взаимнооднозначное соответствие междуинтерфейсом класса и его реализацией. Ни один из них (изначально)не предназначался в качестве базового для получения производных классов.Обычно в иерархии классов конкретные типы стоят особняком. Каждыйконкретный тип можно понять изолированно, вне связи с другими классами.Если реализация конкретного типа удачна, то работающие с ним программысравнимы по размеру и скорости со сделанными вручную программами,в которых используется некоторая специальная версия общего понятия.Далее, если произошло значительное изменение реализации, обычномодифицируется и интерфейс, чтобы отразить эти изменения. Интерфейс,по своей сути, обязан показать какие изменения оказались существеннымив данном контексте. Интерфейс более высокого уровня оставляетбольше свободы для изменения реализации, но может ухудшитьхарактеристики программы. Более того, хорошая реализация зависиттолько от минимального числа действительно существенных классов.Любой из этих классов можно использовать без накладных расходов,возникающих на этапе трансляции или выполнения, и вызванныхприспособлением к другим, "сходным" классам программы. Подводя итог, можно указать такие условия, которым долженудовлетворять конкретный тип: [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, или непосредственно работать с ними с помощью функций, которыеустанавливают и получают значения этих переменных, то почти всегдаэто приводит к плохому результату. Конкретные типы должны быть все-такинастоящими типами, а не просто программной кучей с нескольким функциями,добавленными ради удобства.Абстрактные типы