
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf590 |
Часть III • Программирование с агре |
затем конструктор производного класса переопределяет произведенные в его теле действия. Лучше с помоидью синтаксиса списка инициализации вызвать соответствующий отличный от используемого по умолчанию конструктор базового класса.
Возможно ли создание производного класса без определяемого программистом конструктора? Конечно. Это значит, что ни базовая часть объекта, ни его произ водная часть не требуют инициализации. Если это происходит, следует еще раз проверить программу. Возможно, в ней что-то упущено.
Деструкторы при наследовании
Ниже приведен плохой пример наследования, но он иллюстрирует вопросы, связанные с применением деструкторов в производных классах.
Здесь требовалось разработать класс Address для хранения фамилий и адресов электронной почты. Поскольку наследование является мощным механизмом ор ганизации классов в программе, можно сделать класс Address производным от другого, более простого класса Name, который содержит фамилию человека, имеющего адрес электронной почты. Базовый класс Name включает в себя элемент данных data, ссылающийся на динамически распределяемый массив символов. Конструктор класса динамически распределяет для объекта память и копирует строку-параметр в динамическую память. Деструктор возвращает занятую строкой память в динамически распределяемую область перед уничтожением объекта. Функция get О возвращает указатель на фамилию.
Листинг 13.17. Использование наследования для классов с динамическим управлением памятью
#include <iostream> using namespace std;
class Name { |
|
|
// базовый класс |
char *name; |
|
|
// динамическое управление памятью |
protected: |
|
|
// предотвращает использование объектов |
Name(char nm[]); |
|
|
|
public: |
|
|
// возвращает динамическую память |
"NameO; |
|
; |
|
const char* get() const; } |
// доступ к содержимому |
||
Name::Name(char nm[]) |
|
// выделение динамической памяти |
|
{ name = new char[strlen(nm)+1]; |
|||
if (name ==NULL |
cout « |
"Нет памяти\п"; exit(1); } |
|
strcpy(name,nm); |
|
|
// инициализация динамической области памяти |
const char* Name::get |
() const |
// доступ к закрытым данным |
|
{ return name; } |
|
|
|
Name::~Name() |
|
|
// возвращает данные объекта |
{ delete [] name; } |
|
||
class Address : public Name { |
|
// производный класс |
|
char *email; |
|
|
// нет семантики значений |
Address(const Address&); |
|
||
void operator = (const Address?); |
|
||
public: |
|
|
// выделение динамической памяти |
Address(char name[], char address[]); |
|||
"AddressO; |
|
|
// вывод данных объекта на экран |
void showO const; } ; |
|
I 592 .-_Ж_ |
Часть llf « Протроттировантв с агретроваитвм ш наследованиегу! |
2 . Уничтожается производная часть объекта, и его память (указатель email) возвращается системе.
3 . Вызывается деструктор базового класса, и динамическая память, на которую ссылается указатель name, возвращается системе.
4 . Уничтожается базовая часть объекта, и его память (указатель name) возвращается системе.
Поскольку деструктор класса не имеет параметров, программисту не нужно координировать вызовы деструкторов. Следует лишь убедиться в наличии деструк торов. Отсутствие деструктора приведет к "у'гечкам памяти".
Базовая часть объекта не должна исчезать первой, так как это сервер произ водной части объекта. Для компонентов базового класса может потребоваться сохранять целостность компонентов данных производной части объекта.
Можно объединить управление динамической памятью обоих классов в конст рукторе и деструкторе класса Address, однако разграничение такого управления по классам будет способствовать модульности программы.
Так как пример очень компактен, взаимосвязь между классами здесь большого значения не имеет. Между тем создание производного класса Address из класса Name подчеркивает существующую взаимосвязь. Адрес — не фамилия, но исполь зование наследования предполагает это. Лучше было бы применить композицию классов.
Итоги
в этой главе вы продолжили исследование связей между классами C+ + . Связь наследования позволяет применять один класс как основу для другого класса. Таким путем производный класс наследует все элементы данных и функции-члены базового класса. Обычно в производный класс добавляются также дополнитель ные элементы данных и функции-члены. Иногда в производном классе переопре деляются свойства, унаследованные из базового класса.
Применение наследования — хороший способ повышения модульности про граммы. Вместо проектирования сервера как одного большого фрагмента можно создавать и отлаживать базовые классы, наращивая их функциональность в форме производных классов.
Использование наследования способствует развитию программы. Вместо из менения существующего программного кода можно добавлять новый и затем поддерживать его.
Для применения наследования в C + + вы должны знать множество новых синтаксических деталей. В C + + очень богатая реализация наследования, и часто архитектуру можно осуществить несколькими способами. Это означает, что чрез мерное применение наследования может существенно усложнить программу.
Наследование — хороший инструмент, позволяющий программисту расширить свои возможности и технику разработки ПО. Он может переносить обязанности на серверные классы, передавать в программе идеи разработчика сопрово>едающему приложение программисту. Это новый способ написания программ.
В данной главе рассматривалась лишь часть того, что следует знать о наследо вании в C + + . Следующая глава посвящена технике применения наследования.
^ ^ / ^
ыбор
^
между наследованием и композицией
Темы данной главы
^ Выбор метода повторного использования программ %^ Унифицированный язык моделирования
v^ Учебный пример: магазин проката
у^ Видимость класса и разделение обязанностей «^ Итоги
1^\^ данной главе описывается ряд примеров наследования и композиции.
Ш~!!!^Р2ССмотрим небольшой пример и сравним использование наследования
^<1[[[^^^^ с другими методами программирования.
Попытаемся сравнить использование различных вариантов проектирования для реализации одной и той же программы. В данном случае под "проектированием" подразумевается то же самое, что и в остальной части книги: принятие решения о том, из каких частей (классов) до.лжна состоять программа, и какие обязанности (элементы данных и функции-члены) должны быть назначены каждой части. Сравним различные варианты проектирования и оценим эффективность обш,их методик, сформулированных в главе 1: передача обязанностей от клиентских классов серверным классам, самодокументированные клиентские программы, на писанные в виде вызова серверных методов, и исключение связей между классами. Воспользуемся также специальными критериями низкого уровня: инкапсуляция, сокрытие информации, связность и сцепление.
В данной главе одним из критериев оценки качества проектирования является легкость написания. В этом состоит основной отход от принципов, провозглашен ных в главе 1, где подчеркивалась важность легкости чтения программ и доказы валось, что простота при написании обычно достигается за счет легкости чтения, следовательно, этого нужно избегать. В конце концов программа пишется только один раз, когда набирается ее текст, и реальный набор занимает лишь ничтожную часть времени, затраченного на чтение программы при ее отладке, тестировании, повторном использовании или изменении.
Имейте в виду, что наследование является методикой проектирования, направ ленной на облегчение процесса написания программ. Разработчик серверного класса получает его из базового класса не для того, чтобы улучшить клиентскую
I 594 |
Часть l!l * np/ , |
.:1ие^ |
|
программу^ В идеальном случае разработчик клиентской программы не должен за |
|
|
ботиться о том, спроектирован ли серверный класс "с чистого листа" или получен |
|
|
из некоторого базового, класса (до тех пор, пока серверный класс поддерживает |
|
|
сервисы, которые требуются клиентской программе). |
|
|
Реальное программирование на С4-+ отличается от идеального программиро |
|
|
вания. Использование наследования для облегчения написания серверных классов |
|
|
плохо сочетается с легкостью чтения, как клиентской программы, так и для про |
|
|
граммиста, осуш,ествляюш,его сопровождение. В следуюш^ей главе будет показано, |
|
|
как использовать наследование и для упрощения клиентской программы. |
|
|
Для описания связей между классами используется нотация унифицированного |
|
|
языка моделирования (Unified Modeling Language, UML). На сегодняшний день |
|
|
использование UML рассматривается в качестве решаюш^его фактора достижения |
|
|
успеха в проектировании и реализации объектно-ориентированного продукта. Мно |
|
|
гие организации используют его в своих проектах. UML представляет собой скорее |
|
|
результат политического и технического компромисса, чем шаг вперед в проектиро |
|
|
вании. Язык был разработан комитетом (Object Management Group.— Прим. ред.) |
|
|
с целью унификации некоторых более ранних вариантов нотаций объектно-ориен |
|
|
тированного проектирования, которые добавили возможности более подробного |
|
|
описания связей между объектами. Однако каждый член комитета пытался доба |
|
|
вить в UML новые возможности. В результате язык располагает избыточными |
|
|
возможностями и очень сложен для изучения. |
|
|
Жаль. Язык должен быть ненавязчивым. Разработчикам необходима возмож |
|
|
ность легко выражать свои идеи и понимать без проблем других. Если новичок |
|
|
в данном языке использует неясные или сбивающие с толку операторы, следует |
|
|
предоставить компилятор, который предупреждает об этом разработчика. Ничего |
|
|
подобного в UML нет. Он имеет тенденцию к созданию более сложных диаграмм, |
|
|
чем необходимо. Опыт использования UML показывает, что на его изучение тре |
|
|
буется много времени даже при хорошем знании |
объектно-ориентированного |
|
языка. Кроме того, этот язык моделирования огромен и количество возможных |
|
|
вариантов при проектировании настолько велико, что не стоит пытаться исследо |
|
|
вать их при изучении объектно-ориентированного языка. Однако базовый вариант |
|
|
UML или любой из его предшественников можно и нужно использовать для опи |
|
|
сания объектно-ориентированных проектов, реализованных на языке C+ + . |
В этой главе нотация базового UML представлена как инструмент для сравне ния наследования и композиции. Нотация UML также используется для представ ления общих взаимосвязей между объектами программы. Обсуждаются большие примеры, использующие несколько подходов к их проектированию и реализации. Применение UML будет полезным здесь для понимания сложных вопросов проек тирования программ.
Выбор методики повторного использования кода
Обсудим относительные преимущества и недостатки использования наследо вания и композиции. Обе связи являются отношениями "клиент-сервер". Произ водный класс представляет собой клиента базового класса, а базовый класс — сервер производного класса. Составной класс является клиентом его компонент ного класса, а компонентный класс — сервером составного класса. Это означает, что можно наблюдать значительное сходство между программами C+ + , написан ными с использованием различных методик проектирования.
Общим характерным свойством различных проектных решений является раз деление работы между клиентскими и серверными классами, независимо от того, выполняется ли это с использованием композиции или любых других методик проектирования. Следовательно, серверный класс должен быть реализован до того, как сможет быть спроектирован клиентский класс. Методики, которые об суждаются в данной части, могут использоваться как для разработки, так и для развития программ.
Глава 14 • Выбор между нослвАОванием и композицией |
Г 595~| |
Пример связи ^^клиент-сервер'^ между классами
в качестве примера связи "клиент-сервер" обсуждается приложение, которое использует класс Circle с элементом данных для радиуса круга таким образом, что клиентская программа может послать сообщения для получения доступа к внутреннему представлению данных объектов Circle.
c i r c le с1(2.5), с2(5.0); |
|
/ / |
задание значения радиуса |
double len = с1, getLength |
() ; |
/ / |
вычисление окружности |
double area = c2.getArea(} |
; |
/ / |
доступ к внутренним данным |
c1.set(3.0) ; |
|
|
|
double diam = 2 * c1.getRadius[) |
; |
|
Для поддержки клиентской программы данного вида класс Circle должен реали зовать пять обнледоступных функций-членов:
•Конструктор с одним параметром целого типа
•Метод getLengthO, возвращающий длину окружности
•Метод getRadiusO, возвращающий радиус окружности
• Метод set О, изменяюш^1Й значение радиуса окружности
• Метод getAreaO, вoзвpaщaюш^^й площадь круга
Обратите внимание, что требуется специальная клиентская программа, опреде ляющая, как предположительно должен выглядеть серверный класс. Этот способ не является единственно возможным способом программирования в С-Ы-. При повторном использовании компонентов ПО серверные классы часто проектиру ются как библиотеки классов. В результате для использования этих общих клас сов некоторым клиентам приходится приложить больше усилий.
В данной книге подробно не рассматривается проектирование библиотечных классов. Чтобы хорошо их спроектировать, необходимо обеспечить доступ к внут реннему представлению данных и позволить программистам клиентской части манипулировать данными подходящим, с их точки зрения, способом.
Второй способ программирования в С+ + , поддерживающий связь "клиентсервер", намного сложнее. В этом случае программист серверной части должен учитывать требования клиента и реализовывать методы, которые отвечают дан ным требованиям, а не просто передают клиентской программе информацию для обработки.
Также следует отметить, что для доступа к внутреннему представлению данных серверным объектам посылаются сообщения. Какие бы действия не выполнялись методом класса (например, умножение радиуса окружности на два и на PI для вычисления длины окружности), при этом осуществляется доступ к внутреннему представлению данных (в данном случае к радиусу) по запросу клиентской про граммы, у которой такой доступ отсутствует. Для поддержки требований клиент ской части класс Circle должен выглядеть так:
class Circle |
|
/ / |
исходный код для повторного использования |
|
{ protected: |
|
/ / |
наследование - одна из опций |
|
double |
radius; |
|
/ / |
внутренние данные |
public: |
|
|
|
|
Circle |
(double |
г) |
/ / |
поддержка инициализации |
{ radius = г; } |
|
|
|
|
double getLengthO const |
/ / |
вычисление длины окружности |
||
{ return 2 * PI * radius; |
} |
|
||
double getAreaO |
const |
/ / |
вычисление площади |
{return PI * radius * radius; } double getRadiusO const
{return radius; }
I 596 |
Часть III * Програу|1У1ироЕан11е с агрешрование^ и наследованием |
||
|
void set(double г) |
// изменение размера |
|
|
{ radius = г; } } ; |
||
|
Если выхотите избежать ошибок, связанных с указанием чисел с плавающей |
||
|
точкой, воспользуйтесь другим вариантом класса Circle. |
||
|
class Circle |
// исходный |
код для повторного использования |
|
{ protected: |
// одна изопций - наследование |
|
|
const double PI; |
// должна быть инициализирована в списке |
|
|
double radius; |
// внутренние данные |
|
|
public: |
|
// список функции инициализации |
|
Circle (double r) PI (3.1415926536) |
||
|
{ radius = r; } |
// вычисление длины окружности |
|
|
double getLengthO |
||
|
{ return 2 * PI * radius; |
} |
|
|
double getAreaO |
// вычисление площади |
{return PI * radius * radius; } double getRadiusO const
{return radius; }
void set(double |
r) |
|
{ radius = r; } } |
; |
/ / изменение размера |
Обратите внимание, что указана только одна основная причина использования константы вместо числового литерала — возможность совершения ошибки при наборе одного и того же литерала в различных местах программы. Не обозначена другая распространенная основная причина: удобство изменения значения во время сопровождения. Если не произойдет неожиданное крупное открытие в науке, причин изменения значения PI в ближайшем будундем не видно. Заметьте, что значение PI умножается на два каждый раз при вызове метода getLength(). Это не очень серьезный недостаток, просто подчеркивается, что в данном примере основная цель определения PI как константы состоит в том, чтобы показать еиде раз, что список функции инициализации может содержать не только параметры конструктора, но и литеральные аргументы.
Наконец, учтите, что PI определяется как локальное значение для класса Circle. Если это значение требуется другим классам приложения, они должны либо определить его сами, либо получить из класса Circle. Вы же можете объя вить элемент данных как обндедоступный элемент.
class Circle |
/ / |
исходный код для повторного использования |
|||
{ protected: |
/ / |
одна из опций - |
наследование |
||
double radius; |
/ / |
внутренние данные |
|||
public: |
|
|
|
|
|
const |
double PI; |
/ / |
должна быть инициализирована в списке |
||
public: |
|
|
|
|
|
Circle |
(double r) |
PI (3.1415926536) |
/ / список функции инициализации |
||
{ radius = r; } |
|
|
|
|
|
|
|
/ / |
остальная |
часть |
класса Circle |
Здесь представлен широко распространенный метод определения обш,едоступных данных. Если бы PI было определено в том же разделе общедоступных компо нентов, что и функция-член класса, то здесь ее можно было бы опустить. Теперь класс Circle определен — но так ли это? При такой структуре класса Circle ка>вдому объекту Circle выделяется отдельная область памяти для значения PL Тем не менее это значение одно и то же для всех объектов Circle. Выделение памяти для каждого из них излишне. Программисты, работаюш^ие с необъектноориентированными языками, не сталкиваются с этими вопросами в отличие от программистов C+-f-. Важно развить соответствуюшую интуицию для определе ния потенциально избыточных затрат. А для этого надо внимательно и тш^ательно
598 |
Часть III • Программирование с огрегированием и наследованием |
|
|
Предположим, что программа Circle доступна, но класс Cylinder еиде не спро |
|
|
ектирован. Сходство черт между классами предполагает, что следует попытаться |
|
|
построить класс Cylinder, используя программу класса Ci role, для максимального |
|
|
расширения и облегчения повторного использования имеющейся программы. |
|
|
Для многих такое сходство становится решаюш,им аргументом в пользу насле |
|
|
дования. Это слишком упрош,енный подход. Наследование используется в про |
|
|
граммировании очень широко. Рекомендуем выбрать наследование в результате |
|
|
сравнения с другими методами повторного использования. Как выбрать одну |
|
|
методику вместо другой? |
|
|
Объем и удобство повторного использования кода должны быть главными |
|
|
критериями при выборе методики. Затем надо обратить внимание на объем нового |
|
|
кода, который должен быть написан, и глубину тестирования. Этот пример совсем |
|
|
небольшой, поэтому различия не будут значительными. Однако они покажут, чему |
|
|
следует уделить внимание, принимая решение при выборе способа. |
|
|
Суш^ествуют четыре подхода к повторному использованию кода: повторное ис |
|
|
пользование человеческого интеллекта (т. е. написание кода "с чистого листа"); |
|
|
написание нового класса, чтобы его методы приобретали методы (сервисы) суш,е- |
|
|
ствуюш^его класса; написание нового класса, наследуюш,его суш,ествуюш,ий класс, |
|
|
чтобы его объекты предоставляли клиентам базовые сервисы; и использование |
|
|
наследования с переопределением некоторых методов. Для данного примера пред |
|
|
ставим перечень основных операций: |
|
|
1. |
Человеческий интеллект: напишите новую программу |
|
|
для класса Cylinder "с нуля", используя редактор для копирования |
|
|
в класс Cylinder из программы Circle radius, getLengthO |
|
|
и других функций-членов. Добавьте новую программу Cylinder |
|
|
для выполнения задачи, не предусмотренной классом Circle. |
|
2. |
Приобретение сервисов: используя предположение о том, |
|
|
что каждый цилиндр "включает в себя" круг в качестве объекта, |
|
|
спроектируйте класс Cylinder как составной класс. Объект типа |
|
|
Circle используется как элемент данных класса Cylinder, |
|
|
а методы Cylinder (например, getLengthO) посылают сообщ,ения |
|
|
с таким же именем компоненту Circle. |
|
3. |
Наследование из суш.ествуюш,его класса как базового класса: |
|
|
используя предположение о том, что каждый объект-цилиндр |
|
|
является кругом, спроектируйте класс Cylinder как класс, |
|
|
полученный из Circle. При этом не требуется писать программу |
|
|
для методов наследования, например getLengthO, поскольку |
|
|
каждый объект Cylinder может ответить на эти сообш,ения, |
|
|
унаследованные из базового класса Circle. |
|
4. |
Наследование, но с переопределением некоторых методов: этот подход |
|
|
поддерживает новый способ выполнения сущ^ествующих операций, |
|
|
например плоидадь цилиндра должна вычисляться другим способом, |
|
|
исходя из площади круга. |
В следуюш,их разделах класс Cylinder реализован с использованием этих методов. Будут показаны преимуш.ества и недостатки каждого подхода.
Повторное использование результатов интеллектуальной деятельности
Повторное использование человеческого интеллекта — обычное явление в про граммировании, не используюш.ем объектно-ориентированный подход. Кажется, в объектно-ориентированных языках программисты настолько вдохновлены ис пользованием наследования и композиции, что смотрят свысока на старые методы повторного использования программ.
Глава 14 • Выбор между наследованием и композицией |
599 I |
|||
|
В этом подходе используется прошлый опыт. Программа, написанная ранее, |
|||
воспроизводится и редактируется в соответствии с новыми требованиями. Пред |
||||
полагается, что недавно был написан и протестирован класс Circle, а теперь |
||||
требуется написать класс Cylinder. Такой подход называется повторным исполь |
||||
зованием человеческого интеллекта, потому что повторно используются знания, |
||||
накопленные при написании подобной программы. |
|
|||
|
В листинге 14.1 представлена структура класса Cylinder, который использует |
|||
структуру класса |
Circle. Здесь воспроизводится часть данных класса |
Circle |
||
(в данном случае элемент данных radius) и добавляется то, что требуется для |
||||
класса Cylinder (элемент данных height). Воспроизводится конструктор, добав |
||||
|
|
ляются параметр и программа инициализации элемента |
||
Окружность первого цилиндра: 15.708 |
данных height. Копируются методы, которые могут повторно |
|||
Объем второго цилиндра: |
589.049 |
использоваться слово в слово (get Length () и другие). Реали |
||
Диаметр первого цилиндра: |
6 |
зуются методы класса Cylinder, отсутствующие в классе |
||
|
|
|||
Рис. 14.1. Вывод для |
программы, |
Cylinder (метод VolumeO). При этом не обращается внима |
||
ние на м.етоды класса Circle, которые не требуются в классе |
||||
приведенный |
Cylinder (метод getAreaO). Результаты выполнения про |
|||
из листинга |
14.1 |
|||
граммы представлены на рис. 14.1. |
|
|||
|
|
|
Л и с т и нг 14.1. Пример повторного использования программы посредством человеческого интеллекта
#include <iostream> using namespace std;
class Cylinder |
|
|
// новый класс Cylinder |
|
{ protected: |
|
|
// из класса Circle |
|
static |
const |
double PI; |
||
double |
radius |
; |
|
// из класса Circle |
double |
height; |
|
// новая программа |
|
public: |
|
|
r, double h) |
// из класса Circle плюс новая программа |
Cylinder (double |
||||
{ radius = r; |
|
// новая программа |
||
height = h; } |
|
|||
double getLength |
( )const |
// из класса Circle |
||
{ return 2 * PI * radius ; } |
||||
double getRadius ( )const |
// из класса Circle |
|||
{ return radius; } |
|
|||
void set (double r) |
// из класса Circle |
|||
{ radius = r; } |
|
|
||
double getVolumeO const |
// без getArea () |
|||
{ return PI * radius * radius * height ; |
// новая программа |
|||
const double Cylinder: :PI = 3.1415926536; |
|
|||
int mainO |
|
|
|
|
{ |
|
|
|
// инициализация данных |
Cylinder cyl1(2.5,6.0), cyl2 (5.0,7.5) |
||||
double length = cyl1.getLength(); |
// подобно Circle |
|||
cyl1.set(3.0) ; |
|
|
// отсутствует вызов getAreaO |
|
double diam = 2 * cyll .getRadiusO; |
||||
double vol = cyl2.getVolume() ; |
// отсутствует в классе Circle |
|||
cout « " Окружность первого цилиндра: " « |
length « endl; |