Программируемые отношения




Конкретный язык программирования не может прямо поддерживатьлюбое понятие любого метода проектирования. Если язык программированияне способен прямо представить понятие проектирования, следуетустановить удобное отображение конструкций, используемых в проекте,на языковые конструкции. Например, метод проектирования можетиспользовать понятие делегирования, означающее, что всякаяоперация, которая не определена для класса A, должна выполнятьсяв нем с помощью указателя p на соответствующий член класса B,в котором она определена. На С++ нельзя выразить это прямо. Однако,реализация этого понятия настолько в духе С++, что легко представитьпрограмму реализации: class A { B* p; //... void f(); void ff(); }; class B { //... void f(); void g(); void h(); }; Тот факт, что В делегирует A с помощью указателя A::p,выражается в следующей записи: class A { B* p; // делегирование с помощью p //... void f(); void ff(); void g() { p->g(); } // делегирование q() void h() { p->h(); } // делегирование h() }; Для программиста совершенно очевидно, что здесь происходит, однако здесьявно нарушается принцип взаимнооднозначного соответствия. Такие"программируемые" отношения трудно выразить на языках программирования,и поэтому к ним трудно применять различные вспомогательные средства.Например, такое средство может не отличить "делегирование" от Bк A с помощью A::p от любого другого использования B*. Все-таки следует всюду, где это возможно, добиватьсявзаимнооднозначного соответствия между понятиями проекта и понятиямиязыка программирования. Оно дает определенную простоту и гарантирует,что проект адекватно отображается в программе, что упрощаетработу программиста и вспомогательных средств.Операции преобразований типа являются механизмом, с помощью которогоможно представить в языке класс программируемых отношений, а именно:операция преобразования X::operator Y() гарантирует, что всюду,где допустимо использование Y, можно применять и X. Такое жеотношение задает конструктор Y::Y(X). Отметим, что операцияпреобразования типа (как и конструктор) скорее создает новый объект,чем изменяет тип существующего объекта. Задать операцию преобразованияк функции Y - означает просто потребовать неявного примененияфункции, возвращающей Y. Поскольку неявные применения операцийпреобразования типа и операций, определяемых конструкторами, могутпривести к неприятностям, полезно проанализировать их в отдельностиеще в проекте. Важно убедиться, что граф применений операций преобразования типане содержит циклов. Если они есть, возникает двусмысленная ситуация,при которой типы, участвующие в циклах, становятся несовместимыми вкомбинации. Например: class Big_int { //... friend Big_int operator+(Big_int,Big_int); //... operator Rational(); //... }; class Rational { //... friend Rational operator+(Rational,Rational); //... operator Big_int(); }; Типы Rational и Big_int не так гладко взаимодействуют, как можнобыло бы подумать: void f(Rational r, Big_int i) { //... g(r+i); // ошибка, неоднозначность: // operator+(r,Rational(i)) или // operator+(Big_int(r),i) g(r,Rational(i)); // явное разрешение неопределенности g(Big_int(r),i); // еще одно } Можно было бы избежать таких "взаимных" преобразований, сделавнекоторые из них явными. Например, преобразование Big_int к типуRational можно было бы задать явно с помощью функции make_Rational()вместо операции преобразования, тогда сложение в приведенномпримере разрешалось бы как g(BIg_int(r),i). Если нельзя избежать"взаимных" операций преобразования типов, то нужно преодолеватьвозникающие столкновения или с помощью явных преобразований (как былопоказано), или с помощью определения нескольких различных версийбинарной операции (в нашем случае +).

Компоненты

В языке С++ нет конструкций, которые могут выразить прямо в программепонятие компонента, т.е. множества связанных классов. Основнаяпричина этого в том, что множество классов (возможно с соответствующимиглобальными функциями и т.п.) может соединяться в компонент посамым разным признакам. Отсутствие явного представления понятия вязыке затрудняет проведение границы между информацией (имена),используемой внутри компонента, и информацией (имена), передаваемойиз компонента пользователям.В идеале, компонент определяется множеством интерфейсов, используемыхдля его реализации, плюс множеством интерфейсов, представляемыхпользователем, а все прочее считается "спецификой реализации" идолжно быть скрыто от остальных частей системы. Таково может бытьв действительности представление о компоненте у разработчика.Программист должен смириться с тем фактом, что С++ не даетобщего понятия пространства имен компонента, так что егоприходится "моделировать" с помощью понятий классов и едиництрансляции, т.е. тех средств, которые есть в С++ для ограниченияобласти действия нелокальных имен. Рассмотрим два класса, которые должны совместно использоватьфункцию f() и переменную v. Проще всего описать f и v какглобальные имена. Однако, всякий опытный программист знает, чтотакое "засорение" пространства имен может привести в конце концовк неприятностям: кто-то может ненарочно использовать имена f или vне по назначению или нарочно обратиться к f или v,прямо используя "специфику реализации" и обойдя тем самым явныйинтерфейс компонента. Здесь возможны три решения: [1] Дать "необычные" имена объектам и функциям, которые не рассчитаны на пользователя. [2] Объекты или функции, не предназначенные для пользователя, описать в одном из файлов программы как статические (static). [3] Поместить объекты и функции, не предназначенные для пользователя, в класс, определение которого закрыто для пользователей.Первое решение примитивно и достаточно неудобно для создателяпрограммы, но оно действует: // не используйте специфику реализации compX, // если только вы не разработчик compX: extern void compX_f(T2*, const char*); extern T3 compX_v; //... Такие имена как compX_f и compX_v вряд ли могут привести к коллизии, а натот довод, что пользователь может быть злоумышленником и использоватьэти имена прямо, можно ответить, что пользователь в любом случае можетоказаться злоумышленником, и что языковые механизмы защиты предохраняютот несчастного случая, а не от злого умысла. Преимущество этогорешения в том, что оно применимо всегда и хорошо известно. В то жевремя оно некрасиво, ненадежно и усложняет ввод текста. Второе решение более надежно, но менее универсально: // специфика реализации compX: static void compX_f(T2* a1, const char *a2) { /*... */ } static T3 compX_v; //... Трудно гарантировать, что информация, используемая в классах одногокомпонента, будет доступна только в одной единице трансляции,поскольку операции, работающие с этой информацией, должныбыть доступны везде. Это решение может к тому же привести кгромадным единицам трансляции, а в некоторых отладчиках для С++не организован доступ к именам статических функций и переменных.В то же время это решение надежно и часто оптимально для небольшихкомпонентов.Третье решение можно рассматривать как формализацию и обобщениепервых двух: class compX_details { // специфика реализации compX public: static void f(T2*, const char*); static T3 v; //... }; Описание compX_details будет использовать только создатель класса,остальные не должны включать его в свои программы. В компоненте конечно может быть много классов, не предназначенныхдля общего пользования. Если их имена тоже рассчитаны только налокальное использование, то их также можно "спрятать" внутриклассов, содержащих специфику реализации: class compX_details { // специфика реализации compX. public: //... class widget { //... }; //... }; Укажем, что вложенность создает барьер для использования widgetв других частях программы. Обычно классы, представляющиеясные понятия, считаются первыми кандидатами на повторноеиспользование, и, значит составляют часть интерфейса компонента,а не деталь реализации. Другими словами, хотя для сохранениянадлежащего уровня абстракции вложенные объекты, используемые дляпредставления некоторого объекта класса, лучше считать скрытымидеталями реализации, классы, определяющие такие вложенные объекты,лучше не делать скрытыми, если они имеют достаточную общность.Так, в следующем примере упрятывание, пожалуй, излишне: class Car { class Wheel { //... }; Wheel flw, frw, rlw, rrw; //... }; Во многих ситуациях для поддержания уровня абстракции понятиямашины (Car) следует упрятывать реальные колеса (класс Wheel),ведь когда вы работаете с машиной, вы не можете независимо от нееиспользовать колеса. С другой стороны, сам класс Wheel являетсявполне подходящим для широкого использования, поэтому лучшевынести его определение из класса Car: class Wheel { //... }; class Car { Wheel flw, frw, rlw, rrw; //... }; Использовать ли вложенность? Ответ на этот вопрос зависитот целей проекта и общности используемых понятий. Как вложенность,так и ее отсутствие могут быть вполне допустимыми решениями для данногопроекта. Но поскольку вложенность предохраняет от засоренияобщего пространства имен, в своде правил ниже рекомендуетсяиспользовать вложенность, если только нет причин не делать этого. Отметим, что заголовочные файлы дают мощное средство дляразличных представлений компонент разным пользователям, и они жепозволяют удалять из представления компонента для пользователя теклассы, которые связаны со спецификой реализации. Другим средством построения компонента и представления егопользователю служит иерархия. Тогда базовый класс используется какхранилище общих данных и функций. Таким способом устраняетсяпроблема, связанная с глобальными данными и функциями, предназначеннымидля реализации общих запросов классов данного компонента.С другой стороны, при таком решении классы компонента становятсяслишком связанными друг с другом, а пользователь попадает в зависимостьот всех базовых классов тех компонентов, которые ему действительнонужны. Здесь также проявляется тенденция к тому, что члены,представляющие "полезные" функции и данные "всплывают" к базовомуклассу, так что при слишком большой иерархии классов проблемы сглобальными данными и функциями проявятся уже в рамках этой иерархии.Вероятнее всего, это произойдет для иерархии с одним корнем, а дляборьбы с этим явлением можно применять виртуальные базовые классы($$6.5.4). Иногда лучше выбрать иерархию для представления компонента,а иногда нет. Как всегда сделать выбор предстоит разработчику.

Интерфейсы и реализации

Идеальный интерфейс должен - представлять полное и согласованное множество понятий для пользователя, - быть согласованным для всех частей компонента, - скрывать специфику реализации от пользователя, - допускать несколько реализаций, - иметь статическую систему типов, - определяться с помощью типов из области приложения, - зависеть от других интерфейсов лишь частично и вполне определенным образом.Отметив необходимость согласованности для всех классов, которыеобразуют интерфейс компонента с остальным миром, мы можем упроститьвопрос интерфейса, рассмотрев только один класс, например: class X { // пример плохого определения интерфейса Y a; Z b; public: void f(const char*...); void g(int[],int); void set_a(Y&); Y& get_a(); }; В этом интерфейсе содержится ряд потенциальных проблем: -Типы Y и Z используются так, что определения Y и Z должны быть известны во время трансляции. - У функции X::f может быть произвольное число параметров неизвестного типа (возможно, они каким-то образом контролируются "строкой формата", которая передается в качестве первого параметра). - Функция X::g имеет параметр типа int[]. Возможно это нормально, но обычно это свидетельствует о том, что определение слишком низкого уровня абстракции. Массив целых не является достаточным определением, так как неизвестно из скольких он может состоять элементов. - Функции set_a() и get_a(), по всей видимости, раскрывают представление объектов класса X, разрешая прямой доступ к X::a. Здесь функции-члены образуют интерфейс на слишком низком уровнеабстракции. Как правило классы с интерфейсом такого уровня относятсяк специфике реализации большого компонента, если они вообще могутк чему-нибудь относиться. В идеале параметр функции из интерфейсадолжен сопровождаться такой информацией, которой достаточнодля его понимания. Можно сформулировать такое правило: надо уметьпередавать запросы на обслуживание удаленному серверу по узкомуканалу. Язык С++ раскрывает представление класса как часть интерфейса.Это представление может быть скрытым (с помощью private илиprotected), но обязательно доступным транслятору, чтобы он мог разместитьавтоматические (локальные) переменные, сделать подстановку телафункции и т.д. Отрицательным следствием этого является то, чтоиспользование типов классов в представлении класса может привести квозникновению нежелательных зависимостей. Приведет ли использованиечленов типа Y и Z к проблемам, зависит от того, каковы в действительноститипы Y и Z. Если это достаточно простые типы, наподобие complex илиString, то их использование будет вполне допустимым в большинстве случаев.Такие типы можно считать устойчивыми, и необходимость включатьопределения их классов будет вполне допустимой нагрузкой для транслятора.Если же Y и Z сами являются классами интерфейса большогокомпонента (например, типа графической системы или системы обеспечениябанковских счетов), то прямую зависимость от них можно считатьнеразумной. В таких случаях предпочтительнее использовать член,являющийся указателем или ссылкой: class X { Y* a; Z& b; //... }; При этом способе определение X отделяется от определений Y и Z, т.е.теперь определение X зависит только от имен Y и Z. Реализация X,конечно, будет по-прежнему зависеть от определений Y и Z, но этоуже не будет оказывать неблагоприятного влияния на пользователей X. Вышесказанное иллюстрирует важное утверждение: У интерфейса,скрывающего значительный объем информации (что и должен делать полезныйинтерфейс), должно быть существенно меньше зависимостей, чему реализации, которая их скрывает. Например, определение класса Xможно транслировать без доступа к определениям Y и Z. Однако,в определениях функций-членов класса X, которые работают соссылками на объекты Y и Z, доступ к определениям Y и Z необходим.При анализе зависимостей следует рассматривать раздельнозависимости в интерфейсе и в реализации. В идеале для обоих видовзависимостей граф зависимостей системы должен быть направленнымнецикличным графом, что облегчает понимание и тестированиесистемы. Однако, эта цель более важна и чаще достижима дляреализаций, чем для интерфейсов. Отметим, что класс определяет три интерфейса: class X { private: // доступно только для членов и друзей protected: // доступно только для членов и друзей, а также // для членов и друзей производных классов public: // общедоступно }; Члены должны образовывать самый ограниченный из возможных интерфейсов.Иными словами, член должен быть описан как private, если нетпричин для более широкого доступа к нему; если же таковые есть, точлен должен быть описан как protected, если нет дополнительных причинзадать его как public. В большинстве случаев плохо задавать все данные,представляемые членами, как public. Функции и классы, образующие общийинтерфейс, должны быть спроектированы таким образом, чтобы представлениекласса совпадало с его ролью в проекте как средства представленияпонятий. Напомним, что друзья являются частью общего интерфейса. Отметим, что абстрактные классы можно использовать дляпредставления понятия упрятывания более высокого уровня ($$1.4.6,$$6.3, $$13.3).

Свод правил

В этой главе мы коснулись многих тем, но, как правило, избегалидавать настоятельные и конкретные рекомендации по рассматриваемымвопросам. Это отвечает моему убеждению, что нет "единственно верногорешения". Принципы и приемы следует применять способом, наиболееподходящим для конкретной задачи. Здесь требуются вкус, опыт иразум. Тем не менее, можно предложить свод правил, которыеразработчик может использовать в качестве ориентиров, пока неприобретет достаточно опыта, чтобы выработать лучшие.Этот свод правил приводится ниже. Он может служить отправной точкой в процессе выработкиосновных направлений проекта конкретной задачи, или же он можетиспользоваться организацией в качестве проверочного списка. Подчеркнуеще раз, что эти правила не являются универсальными и не могутзаменить собой размышления. - Нацеливайте пользователя на применение абстракции данных и объектно-ориентированного программирования. - Постепенно переходите на новые методы, не спешите. - Используйте возможности С++ и методы обЪектно-ориентированного программирования только по мере надобности. _ Добейтесь соответствия стиля проекта и программы. - Концентрируйте внимание на проектировании компонента. _ Используйте классы для представления понятий. - Используйте общее наследование для представления отношений "есть". - Используйте принадлежность для представления отношений "имеет". - Убедитесь, что отношения использования понятны, не образуют циклов, и что число их минимально. - Активно ищите общность среди понятий области приложения и реализации, и возникающие в результате более общие понятия представляйте как базовые классы. - Определяйте интерфейс так, чтобы открывать минимальное количество требуемой информации: - Используйте, всюду где это можно, частные данные и функции-члены. - Используйте описания 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, или непосредственно работать с ними с помощью функций, которыеустанавливают и получают значения этих переменных, то почти всегдаэто приводит к плохому результату. Конкретные типы должны быть все-такинастоящими типами, а не просто программной кучей с нескольким функциями,добавленными ради удобства.

Абстрактные типы

Самый простой способ ослабить связь между пользователем классаи его создателем, а также между программами, в которых объектысоздаются, и программами, в которых они используются, состоит в введениипонятия абстрактных базовых классов. Эти классы представляютинтерфейс со множеством реализаций одного понятия. Рассмотримкласс 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. В обычной ситуации операции абстрактного класса задаются какчистые виртуальные функции, и такой класс не имеет членов,представляющих данные (не считая скрытого указателя на таблицувиртуальных функций). Это объясняется тем, что добавление невиртуальнойфункции или члена, представляющего данные, потребует определенныхдопущений о классе, которые будут ограничивать возможные реализации.Изложенный здесь подход к абстрактным классам близок по духу традиционнымметодам, основанным на строгом разделении интерфейса и его реализаций.Абстрактный тип служит в качестве интерфейса, а конкретные типыпредставляют его реализации. Такое разделение интерфейса и его реализаций предполагаетнедоступность операций, являющихся "естественными" для какой-тоодной реализации, но не достаточно общими, чтобы войти винтерфейс. Например, поскольку в произвольном множестве нетупорядоченности, в интерфейс set нельзя включать операциюиндексирования, даже если для реализации конкретного множестваиспользуется массив. Это приводит к ухудшению характеристик программыиз-за отсутствия ручной оптимизации. Далее, становится как правилоневозможной реализация функций подстановкой (если не считать каких-токонкретных ситуаций, когда настоящий тип известен транслятору), поэтомувсе полезные операции интерфейса, задаются как вызовывиртуальных функций. Как и для конкретных типов здесь плата заабстрактные типы иногда приемлема, иногда слишком высока. Подводя итог, перечислим каким целям должен служить абстрактный тип: [1] определять некоторое понятие таким образом, что в программе могут сосуществовать для него несколько реализаций; [2] применяя виртуальные функции, обеспечивать достаточно высокую степень компактности и эффективности выполнения программы; [3] сводить к минимуму зависимость любой реализации от других классов; [4] представлять само по себе осмысленное понятие.Нельзя сказать, что абстрактные типы лучше конкретных типов, этопросто другие типы. Какие из них предпочесть - это, как правило,трудный и важный вопрос для пользователя. Создатель библиотеки можетуклониться от ответа на него и предоставить варианты с обеими типами,тем самым выбор перекладывается на пользователя. Но здесь важно яснопонимать, с классом какого вида имеешь дело. Обычно неудачейзаканчивается попытка ограничить общность абстрактного типа, чтобыскорость программ, работающих с ним, приблизила


Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: