Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 195
Текст из файла (страница 195)
В библиотеке конкретных классов нет никакого общего свойства для всех содержащихся в ней типов, так что пользователю приходится вникать в каждый класс по отдельности. Это плата за быстродействие и компактность, причем эта плата может окупиться, а может и не окупиться. В то же время, часто легче понять и использовать индивидуальный конкретный класс, чем более общий и абстрактный. Это характерно для таких широко известных типов данных, как, например, массивы и списки. В идеале лучше скрывать реализацию в максимально возможной степени, не допуская при этом серьезных ухудшений производительности.
Здесь встраиваемые функции могут дать большой выигрыш. Открывать поля данных класса, чтобы пользователь манипулировал ими напрямую или через специально предназначенные для этого функции-члены, редко когда бывает хорошей идеей (924.4.2). Конкретные типы должны оставаться типами, а не мешками битов с несколькими функциями, добавленными лишь для удобства. 25.2. Конкретные классы (типы) 899 25.2.1. Многократное использование конкретных типов Конкретные типы редко когда бывает полезным использовать в виде базовых классов. Каждый конкретный тип призван обеспечить четкое и эффективное представление единственной концепции.
Маловероятно, чтобы класс, хорошо справляющийся с такой задачей, был хорошим кандидатом для создания других, родственных классов посредством открытого наследования. Такие классы лучше использовать в качестве типов полей данных других классов, или в качестве закрытых базовых классов, Тут онн могут использоваться эффективно, не смешивая при этом свои интерфейсы и реализацию с интерфейсами и реализацией новых классов. Рассмотрим наследование от класса Роге: с1аьв Му Фаге: риЫ(с Риге ( УУ ... ): Нужно ли когда-нибудь использовать Му Фаге вместо Разе? Это, конечно, зависит от того, что именно представляет собой Му Фаге, но по моему опыту конкретные классы редко бывают полезны в качестве базовых без серьезных модификаций. Немодифицируемым образом конкретные классы «повторно используются» так же, как и встроенные типы, вроде 1иг (910.3.4).
Например: с!аев Риге апФ гйпе ( реЬаге: Разе Ф) ттте 1; риЫ(с: уу ... )' Такая форма использования (повторного?) проста, эффективна и продуктивна. Может, это ошибка проектирования привела к тому, что класс Риге не пригоден к повторному использования посредством наследования от него производных классов? Иногда слышны утверждения, что каждый класс должен проектироваться с учетом последующей модификации посредством замещения и посредством использования его членов функциями-членами производных классов. Эта точка зрения привела бы к следующему варианту класса Разе; с!аьв Раге2 ( риЫ(с: УУ публичный интерфейс, состоящий преимущественно из виртуальных ф> нкций реогесгеФ: УУ другие детали реализации (возмоясно, включая часть представления) ре(ваге: УУ представление и иные детали реализации Глава 25.
Роли классов 900 Часть представления класса сделана зашишенной с целью облегчить замещение функций в производных классах. Это позволяет сделать тип Раге2 весьма податливым к модификациям посредством наследования, оставляя его открытый пользовательский интерфейс неизменным.
Цена такого решения следующая: 1. Базовые операции менее эффективны. Обращение к виртуальным функциям в С++ немного медленнее, чем к обычным функциям; виртуальные функции редко когда бывает разумным делать встраиваемыми; объекты классов с виртуальными функциями требуют добавочного расхода памяти. 2. Необходимость динамического использования свободной памяти. Цель введения класса Ваге2 — позволить использовать взаимозаменяемым образом объекты классов, производных от Ваге2. Поскольку размеры этих обьектов разные, их нужно очевидным образом размешать в динамически выделяемой свободной памяти, обращаться к которой придется с помощью указателей или ссылок. Локализация данных класса при этом резко уменьшается.
3. Неудобство для пользователей. Выгоды от полиморфизма, обеспечиваемого виртуальными функциями, достигаются лишь при манипулировании объектами класса через указатели или ссылки. 4. Слабая инкапсуляция. Виртуальные функции можно замешать, и зашишенными данными можно манипулировать из производных классов 6!2.4.1.1).
Рассмотренная цена решения в обшем случае не столь уж и велика, и часто это решение оказывается именно тем, что нужно 625.3, Э25.4). В то же время, для таких простых типов, как Раге2, цена явно излишняя и совершенно необязательная. Наконец, четко спроектированный конкретный тип часто подходит для реализации (представления) более гибких (податливых) типов. Например: с1авв ВагеЗ ( риЫзс: У публичный интерфейс, состоящий преимущественно иэ виртуальньп функций рнии ге: Васе а'; Именно этот способ следует использовать при необходимости встроить конкретные типы в иерархию классов. См, также э25.10[Ц. 25.3. Абстрактные типы Простейшим способом ослабить связь между пользователями класса и его разработчиками, а также между создаюшим объекты кодом и кодом, используюшим объекты, является введение абстрактного класса, предоставляюшего интерефейс к набору реализаций некоторой общей концепции. Рассмотрим шаблон Бег (множество): <етр(осе<с!авв 7"> с!аев Лег ( риЫ(с: 25.3.
Абстрактные типы 901 ч!гсиас юЫ спеегт(Т*) = О; чисиа! юЫ гегпоче(Т*) = 0; чогсиас Ьпс и тетвег ( Т* ) = 0; ч!гсиа1 Т*!)гм() =О; ч!гсиас Т* пехс () = 0; чдгсиа! -Яес() () )г Это интерфейс ко множеству со встроенной концепцией итерации по его элементам, Типично отсутствие конструкторов и присутствие виртуального деструктора (912.4.2). Допускаются разные реализации 616.2.1). Например: Сепср!иге<с!акт Т> с1асо Теес еес: риЫ(с Яес<Т>, рг!часе 1!ос< Т> ( // ...
)' гетр!осе<с!ага Т> с1аеа Уессог еес: риЫЫ Яес<Т>, рг!часе чессог<Т> ( // ... )' Абстрактный класс предоставляет интерфейс к любым реализациям. Это означает, что мы можем применять Яес, не зная, какая из реализаций используется. Например: чо!сС Т(Яес<рсапе*>а е) ( )ог(Р!апе** р = е.)ггес(); р; р = е.пехс() ) ( //мой код // ... ) Ейс еес<рсапе*> и; Уессог сес<рсапе*> ч (100) чоЫО() ( Т(зс) с Х(ч): ) Для конкретных типов требуется перепроектирование классов реализации с целью отражения общности и применение шаблонов для эксплуатации этой общности. Здесь же общность отражается интерфейсом (то есть Яес), а от классов реализации не требуется никакой другой общности, кроме способности реализовывать интерфейс.
Более того, пользователи Яес не знают объявлений Хгес зес и Уессог зес, и поэтому пользователи не зависят от этих объявлений и не нуждаются в перекомпиляции в случае, если изменяются ХЫ ее!или Уессог зес, или даже если вводится новая реа- 902 Глава 25. Роли классов лизация Яег — скажем, Тгее зев Все зависимости сосредоточены в функциях, которые явным образом используют классы, производные от Бег. В частности, предполагая традиционное использование заголовочных файлов, программисту, пишущему Г(БеМ), нужно включить лишь Лепя, а не ьмг лег.й или $естог зепя.
«Реализационный» заголовочный файл нужен лишь там, где создаются АЫ лег или Ресгог лег. Пользовательский код можно еше сильнее изолировать от классов реализации, введя абстрактный класс, обрабатывающий запросы на создание обьектов (так называемая «фабрика»; 912,4,4), Отделение интерфейса от реализации означает отсутствие доступа к операциям, свойственным конкретной реализации, но не достаточно общим, чтобы стать частью интерфейса.
Например, поскольку множество неупорядочено по своей сути, мы не можем включить в интерфейс операцию индексации даже в том случае, когда для реализации применяется частное представление в виде массива. Отсюда вытекает некоторая потеря эффективности за счет запрета ручной оптимизации. Кроме того, встраивание функций, как правило, недостижимо (кроме локального контекста, когда компилятор знает истинный тип), а все интересные операции интерфейса представлены виртуальными функциями. Как и в случае с конкретными типами, затраты на абстрактные типы иногда окупаются, а иногда нет.
Подводя итог, можно сказать, что абстрактные типы предназначены для того, чтобьг 1. Определять одну концепцию с разными реализациями, сосуществующими в пределах программы. 2. Обеспечивать приемлемые быстродействие и затраты памяти посредством использования виртуальных функций. 3. Минимизировать зависимость каждой реализации от других классов. 4. Иметь возможность самоописания.
Абстрактные типы не лучше конкретных типов; они просто другие. Разработчики библиотек часто предоставляют и те, и другие типы, оставив выбор за пользователем. Попытки ограничить общность абстрактного типа ради конкуренции с конкретными типами в быстродействии как правило оканчиваются неудачей; это компрометирует возможность использовать взаимозаменяемые реализации без значительной перекомпиляции после их модификации.
Также неудачей оканчиваются и попытки увеличить универсальность конкретных типов; при этом снижается их эффективность и удобство использования. Конкретные и абстрактные типы могут сосуществовать — на самом деле они должны сосуществовать, ибо конкретные типы обеспечивают реализацию абстрактных типов. Однако не следует смешивать эти типы хаотическим образом.