straustrup2 (852740), страница 91
Текст из файла (страница 91)
Каждое понятиевоплощается с помощью основной конструкции - класса. В идеале надо иметь минимальный наборпростых и ортогональных видов классов, исходя из которого можно построить любой полезный иразумно-определенный класс. Идеал нами не достигнут и, возможно, недостижим вообще. Важнопонять, что любой из перечисленных видов классов играет свою роль при проектировании библиотекии, если рассчитывать на общее применение, никакой из них не является по своей сути лучше других.335Бьерн Страуструп.Язык программирования С++В этой главе вводится понятие обширного интерфейса ($$13.6), чтобы выделить некоторый общийслучай всех этих видов классов. С помощью него определяется понятие каркаса области приложения($$13.7).Здесь рассматриваются прежде всего классы, относящиеся строго к одному из перечисленных видов,хотя, конечно, используются классы и гибридного вида.
Но использование класса гибридного видадолжно быть результатом осознанного решения, возникшего при оценке плюсов и минусов различныхвидов, а не результатом пагубного стремления уклониться от выбора вида класса (слишком часто"отложим пока выбор" означает просто нежелание думать).
Неискушенным разработчикам библиотекилучше всего держаться подальше от классов гибридного вида. Им можно посоветовать следоватьстилю программирования той из существующих библиотек, которая обладает возможностями,необходимыми для проектируемой библиотеки. Отважиться на создание библиотеки общегоназначения может только искушенный программист, и каждый создатель библиотеки впоследствиибудет "осужден" на долгие годы использования, документирования и сопровождения своегособственного создания.В языке С++ используются статические типы. Однако, иногда возникает необходимость в дополнение квозможностям, непосредственно предоставляемым виртуальными функциями, получать динамическуюинформацию о типах. Как это сделать, описано в $$13.5.
Наконец, перед всякой нетривиальнойбиблиотекой встает задача управления памятью. Приемы ее решения рассматриваются в $$13.10.Естественно, в этой главе невозможно рассмотреть все методы, оказавшиеся полезными при созданиибиблиотеки. Поэтому можно отослать к другим местам книги, где рассмотрены следующие вопросы:работа с ошибками и устойчивость к ошибкам ($$9.8), использование функциональных объектов иобратных вызовов ($$10.4.2 и $$9.4.3) , использование шаблонов типа для построения классов ($$8.4).Многие темы этой главы связаны с классами, являющимися контейнерами, (например, массивы исписки).
Конечно, такие контейнерные классы являются шаблонами типа (как было сказано в $$1.и 4.3$$8). Но здесь для упрощения изложения в примерах используются классы, содержащие указатели наобъекты типа класс. Чтобы получить настоящую программу, надо использовать шаблоны типа, какпоказано в главе 8.13.2 Конкретные типыТакие классы как vector ($$1.4), Slist ($$8.3), date ($$5.2.2) и complex ($$7.3) являются конкретными втом смысле, что каждый из них представляет довольно простое понятие и обладает необходимымнабором операций. Имеется взаимнооднозначное соответствие между интерфейсом класса и егореализацией.
Ни один из них (изначально) не предназначался в качестве базового для полученияпроизводных классов. Обычно в иерархии классов конкретные типы стоят особняком. Каждыйконкретный тип можно понять изолированно, вне связи с другими классами. Если реализацияконкретного типа удачна, то работающие с ним программы сравнимы по размеру и скорости сосделанными вручную программами, в которых используется некоторая специальная версия общегопонятия.
Далее, если произошло значительное изменение реализации, обычно модифицируется иинтерфейс, чтобы отразить эти изменения. Интерфейс, по своей сути, обязан показать какие измененияоказались существенными в данном контексте. Интерфейс более высокого уровня оставляет большесвободы для изменения реализации, но может ухудшить характеристики программы. Более того,хорошая реализация зависит только от минимального числа действительно существенных классов.Любой из этих классов можно использовать без накладных расходов, возникающих на этапе трансляцииили выполнения, и вызванных приспособлением к другим, "сходным" классам программы. Подводяитог, можно указать такие условия, которым должен удовлетворять конкретный тип:[1]полностью отражать данное понятие и метод его реализации;[2]с помощью подстановок и операций, полностью использующих полезные свойства понятия иего реализации, обеспечивать эффективность по скорости и памяти, сравнимую с "ручнымипрограммами";[3]иметь минимальную зависимость от других классов;[4]быть понятным и полезным даже изолированно.Все это должно привести к тесной связи между пользователем и программой, реализующей конкретныйтип.
Если в реализации произошли изменения, программу пользователя придется перетранслировать,336Бьерн Страуструп.Язык программирования С++поскольку в ней наверняка содержатся вызовы функций, реализуемые подстановкой, а такжелокальные переменные конкретного типа.Для некоторых областей приложения конкретные типы обеспечивают основные типы, прямо непредставленные в С++, например: комплексные числа, вектора, списки, матрицы, даты, ассоциативныемассивы, строки символов и символы, из другого (не английского) алфавита.
В мире, состоящем изконкретных понятий, на самом деле нет такой вещи как список. Вместо этого есть множество списочныхклассов, каждый из которых специализируется на представлении какой-то версии понятия список.Существует дюжина списочных классов, в том числе: список с односторонней связью; список сдвусторонней связью; список с односторонней связью, в котором поле связи не принадлежит объекту;список с двусторонней связью, в котором поля связи не принадлежат объекту; список с одностороннейсвязью, для которого можно просто и эффективно определить входит ли в него данный объект; список сдвусторонней связью, для которого можно просто и эффективно определить входит ли в него данныйобъект и т.д.Название "конкретный тип" (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++){// ваш код}// ...}Существование таких "естественных" для выбранного метода реализации операций обеспечиваетэффективность программы и значительно облегчает ее написание.
К тому же, хотя реализация вызоваподстановкой обычно возможна только для простых операций типа индексации массива или полученияследующего элемента списка, она оказывает значительный эффект на скорость выполненияпрограммы. Загвоздка здесь состоит в том, что фрагменты программы, использующие по своей сутиэквивалентные операции, как, например, два приведенных выше цикла, могут выглядеть непохожимидруг на друга, а фрагменты программы, в которых для эквивалентных операций используются разныеконкретные типы, не могу заменять друг друга.
Обычно, вообще, невозможно свести сходныефрагменты программы в один.Пользователь, обращающийся к некоторой функции, должен точно указать тип объекта, с которымработает функция, например:337Бьерн Страуструп.Язык программирования С++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);your(sl);}// теперь нормально: вызов my(vector&)// теперь нормально: вызов your(slist&)Поскольку тело функции существенно зависит от типа ее параметра, надо написать каждую версиюфункций my() и your() независимо друг от друга, что может быть хлопотно.С учетом всего изложенного конкретный тип, можно сказать, походит на встроенные типы.Положительной стороной этого является тесная связь между пользователем типа и его создателем, атакже между пользователями, которые создают объекты данного типа, и пользователями, которыепишут функции, работающие с этими объектами.
Чтобы правильно использовать конкретный тип,пользователь должен разбираться в нем детально. Обычно не существует каких-то универсальныхсвойств, которыми обладали бы все конкретные типы библиотеки, и что позволило бы пользователю,рассчитывая на эти свойства, не тратить силы на изучение отдельных классов. Такова плата закомпактность программы и эффективность ее выполнения. Иногда это вполне разумная плата, иногданет. Кроме того, возможен такой случай, когда отдельный конкретный класс проще понять ииспользовать, чем более общий (абстрактный) класс. Именно так бывает с классами, представляющимихорошо известные типы данных, такие как массивы или списки.Тем не менее, укажем, что в идеале надо скрывать, насколько возможно, детали реализации, пока этоне ухудшает характеристики программы.