Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
278
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

I

600 I

 

Часть Hi ^ Прогрол^^ировоние с агрегирование

 

cout

«

" Объем второго цилиндра: " «

vol

«

endl;

 

cout

«

" Диаметр первого цилиндра: " « diam « end1;

 

return

0;

 

 

 

 

}

 

 

 

 

 

 

 

 

Большая часть существующей программы Circle (данные и методы) скопиро­

 

 

 

вана дословно. Ненужные методы не включены. Для данных и методов, пропущен­

 

 

 

ных в Circle, должна быть разработана новая программа, которая представлена

 

 

 

в классе Cylinder. Ее нужно протестировать. Если существующая программа ко­

 

 

 

пируется с помощью текстового редактора, а не набирается с клавиатуры, тести­

 

 

 

рование программы Circle должно быть минимальным. Поскольку интерфейсы

 

 

 

функций Circle не изменились, то тестирование для класса Circle может повтор­

 

 

 

но использоваться и для класса

Cylinder.

 

 

 

Применение этого метода способствует повышению производительности про­

 

 

 

граммы. Все будут ошеломлены молниеносной скоростью разработки программ.

 

 

 

Если ваши коллеги узнают, что метод основывается на предыдущем опыте, они

 

 

 

уже не будут так восхищаться. С другой стороны, вас приняли для выполнения

 

 

 

этой работы, потому что у вас есть опыт разработки подобных систем.

 

 

 

С точки зрения программной инженерии подобный подход имеет существенные

 

 

 

недостатки. Классы Circle и Cylinder связаны друг с другом. У них общие эле­

 

 

 

менты данных и общие функции-члены. Связь между классами Circle и Cylinder

 

 

 

существует только в сознании проектировщика Cylinder. Программист, осуществ­

 

 

 

ляющий сопровождение, может легко ее пропустить, что приведет к ошибкам.

 

Повторное использование

 

 

 

посредством покупки сервисов

 

 

 

Хорошей практикой считается написание программ С4-+ таким образом, что­

 

 

 

бы объекты, отправляющие сообщения другим объектам в программе, были

 

 

 

связаны друг с другом в реальной жизни. Отправка сообщения другому объекту

 

 

 

иногда называется покупкой

сервисов этого объекта.

 

 

 

Обратите внимание, что объекты посылают сообщения другим объектам про­

 

 

 

граммы, а не друг другу.

Синтаксически вполне возможно, что объект класса А

 

 

 

посылает сообщение объекту класса В, а объект класса В посылает сообщение

 

 

 

объекту класса А в одной и той же программе. C + + допускает такое запутанное

 

 

 

сотрудничество. Более того, для некоторых программ такая архитектура может

 

 

 

быть полезной. Однако в результате это приводит к ненужному усложнению

 

 

 

структуры. Именно поэтому в большинстве случаев взаимодействия классов

 

 

 

имеется один класс, играющий роль клиентского класса, и существует другой

 

 

 

класс, играющий роль серверного класса. Когда метод клиентского класса отправ­

 

 

 

ляет сообщение объекту серверного класса, говорится, что один класс "покупает

 

 

 

сервисы" другого класса.

 

 

 

Существуют три ситуации, в которых клиентский метод может осуществить доступ к объекту-серверу и отправить ему сообщение:

Определение объекта-сервера как локальной переменной в клиентском методе.

Определение объекта-сервера как элемента данных в клиентском классе.

Получение объекта-сервера как параметра клиентского метода.

Первая ситуация выгодна с точки зрения обмена сообщениями между класса­ ми: только одна клиентская функция (где определен объект-сервер) имеет доступ к объекту-серверу. Всегда следует выбирать такой тип связей "клиент-сервер". Часто это невозможно, поскольку доступ к объекту-серверу должен осуществляться с помощью других методов клиентского класса или других классов.

Глава 14 • Выбор между наследование!^! и композицией

601

Рассмотрим вторую ситуацию. Объект-сервер доступен всем функциям-членам клиентского класса. Если есть выбор, применяйте тип связи "клиент-сервер", а не используйте объект-сервер как параметр метода клиентского класса.

Третья ситуация наиболее сложная с точки зрения обмена сообщениями между взаимодействуюидими классами: объект-аргумент используется как сервер функцией, к которой он передается как параметр, и функциями, вызываюндими эту серверную функцию. Избегайте этого типа связей. Старайтесь обратиться либо к первой, либо ко второй ситуации (доступ методами только одного клиент­ ского класса).

С точки зрения повторного использования кода именно второй тип связей "клиент-сервер" позволяет создавать серверные классы, которые служат их кли­ ентам, обеспечивая сервисами суидествующих классов.

В примере связей между классами Circle и Cylinder объект Circle становится членом класса Cylinder. Поскольку делается попытка спроектировать элементы данных как закрытые (или защищенные), сервисы Circle недоступны клиентам Cylinder непосредственно. Для предоставления таких сервисотз клиентам (в дан­ ном случае метод getLengthO), класс Cylinder должен запрашивать элемент данных Circle.

В листинге 14.2 показана эта структура (вывод программы такой же, как и в листинге 14.1). Класс Circle определен явно. Класс Cylinder задает элемент данных класса Circle вместе с дополнительными данными (элемент данных height). Если этот элемент данных был объявлен как public, он доступен клиентской программе Cylinder.

Class Cylinder

 

/ /

новый класс Cylinder

{ protected:

 

 

 

 

 

 

double

height;

 

/ /

новая программа

public:

 

 

 

 

 

 

 

Circle

с;

 

 

/ /

без

PI, без радиуса

public:

 

 

 

 

 

 

 

Cylinder

(double r,

double h)

/ /

Из Circle плюс новая программа

: c(r)

 

/ /

список функции инициализации (без PI)

{ height

= h; }

 

/ /

новая программа

double getVolumeO

const

/ /

без

getAreaO

{ double

radius = c.getRadiusO;

 

 

/ /

новая программа

return

Circle::PI

* radius * radius *

height;

}

} ;

 

 

 

/ /

без getLengthO, getRadiusO, set()

Класс Cylinder содержит всего лишь несколько функций-членов. Он не вклю­ чает методы реализации getLengthO, getRadiusO и set(), потому что клиентская программа может направить эти сообщения общедоступному элементу данных из класса Cylinder.

Cylinder су11(2.5,6.0) , су12(5.О,7.5);

/ / инициализация данных

double length = су11.с.getLengthO;

/ /

использование элемента данных Circle

cyl1.c.set(3.0);

double diam = 2 * cyl1.с.getRadiusO; double vol = cyl2.get\/olume();

/ /

без вызова

getAreaO

/ /

отсутствует

в Circle

Каковы недостатки этой структуры? Данные общедоступны. Клиент использует имя элемента данных класса Cylinder и зависит от структуры класса Cylinder. Чтобы выразить беспокойство о качестве этой структуры, следует упомянуть о раз­ делении обязанностей между классом Cylinder и его клиентской программой. Здесь проектировщику класса Cylinder живется легко. Класс Cylinder только предоставляет метод getVolumeO и уклоняется от любых других обязанностей. К какому классу принадлежат методы getLengthO, getRadiusO и setO? Об этих методах знает клиентская программа, но не ее серверный класс Cylinder. Эти сообщения посылает клиентская программа, а не класс Cylinder.

602

Часть II! • Програтттрошаитв с агре-

насдеАОвание1У1

 

 

Цепочный синтаксис для вызова функций в клиентской программе неудобен,

 

однако проблема касается расширения обязанностей проектировщика клиентской

 

программы. Проектировщик (и специалист, отвечающий за сопровождение этой

 

программы) должен изучить сервисы классов Circle и Cylinder. В данном приме­

 

ре определения классов удобно размещены вместе. В реальной жизни их можно

 

разделить. Кроме того, может быть больше двух классов, связанных друг с другом.

 

Также могут отсутствовать какие-либо указания на то, что эти классы (Circle

 

и Cylinder) связаны друг с другом.

 

 

Именно поэтому структура в листинге 14.2 — лучший пример покупки серви­

 

сов.

Элемент данных Circle

не является общедоступным в классе Cylinder.

 

(Он защищен, но для клиентской программы недосягаем.) В результате именно

 

класс Cylinder, а не его клиент, знает, к какому классу принадлежат методы

 

getLengthO, getRadiusO и set(). Класс Cylinder определяет множество одно-

 

строчников. Единственной задачей этих функций-членов является выполнение

 

двустороннего обмена и отправление сообщения с тем же именем элементу дан­

 

ных из класса Cylinder.

 

 

Листинг 14.2. Пример повторного использования программы

 

посредством покупки сервисов элемента данных (композиция класса)

include <iostream>

 

 

 

using

namespace std

;

 

 

class

Circle

 

// исходный

код для повторного использования

{ protected:

 

// одна изопций - наследование

double radius;

 

// внутренние данные

public-

 

 

 

static const double PI

// требуется

инициализировать

public:

 

 

 

Circle (double r)

// конструктор преобразования

{

radius = r ;

}

 

 

double getLengthO const

// вычисление длины окружности

{

return 2 * PI * radius; }

 

 

double getAreaO

const

// вычисление площади

{

return PI * radius * radius ; }

 

 

double getRadiusO const

 

 

{

return radius; }

 

 

void set(double r.)

// изменение размера

{

radius = r; }

};

const double Circle::PI = 3.1415926536;

 

 

class Cylinder

 

// новый класс Cylinder

{ protected:

 

// неуказывается PI, необозначается радиус

Circle c;

 

double height;

 

// новая программа

public:

r, double h)

// изCircle плюс новая программа

Cylinder (double

 

: c(r)

 

// список функции инициализации (кроме PI)

{ height = h; }

 

// новая программа

double getLengthO const

// из класса Circle

{

return c.getLengthO; }

double getRadiusO const

// из класса Circle

{

return c.getRadiusO; }

 

 

 

Глава 14 « Выбор между наследованием и композицией

603

void seKdouble г)

// из класса Circle

 

{ c.set(r); }

 

 

double getVolumeO const

// без getAreaO

 

{ double radius = c.getRadiusO;

// новая программа

 

return Circle::PI * radius * radius * height; }

 

} ;

 

 

 

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 * cyl1.getRadius();

 

double vol = cyl2.getVolume();

// отсутствует в Circle

 

cout «

" Circumference of first cylinder: " « length « endl;

 

cout «

" Volume of the second cylinder: " «

vol « endl;

 

cout «

" Diameter of the first cylinder: " « diam « endl;

 

return 0;

}

В этом варианте хорошо поддерживаются как инкапсуляция данных, так и раз­ деление обязанностей. Проектировщик клиентской программы должен знать только сервер Cylinder, а не структуру класса Circle (сервер серверного класса). Связь между классами ясна во время сопровождения.

Метод повторного использования программы может быть быстрее, чем по­ вторная запись "с нуля". Тесты менее требовательны — однострочники легко тестировать. Нет необходимости использовать наследование и связь между базо­ выми и производными классами.

Проблема может возникнуть из-за необходимости доступа к элементам данных Circle из класса Cylinder. Важно, что класс Circle обеспечивает методы доступа, необходимые классу Cylinder. Некоторые программисты C + + не расположены к быстрому распространению однострочников. Они просты, но слишком надоедли­ вы. Использование наследования исключает эту проблему.

Повторное использование программы с помощью наследования

в настояш,ее время наиболее популярным методом является повторное исполь­ зование программы посредством наследования. Большинство базовых сервисов могут наследоваться "как есть". В таких ситуациях этот метод работает хорошо и исключает большинство одностроковых методов, что типично для композиции класса.

В листинге 14.3 показан пример повторного использования программы для класса Circle. Он определяется в качестве базового класса для производного класса Cylinder. Поскольку клиентская программа такая же, как и в листин­ гах 14.1 и 14.2, не удивительно, что вывод для программы соответствует выводу на рис. 14.1.

В предыдуш^ей версии, в листинге 14.2, предполагалось, что цилиндр "имеет" круг. Однако класс Cylincfer реализовал метод, общий для обоих классов, отправ­ ляющих сообщения элементу данных класса Circle. В этой версии программы предполагается, что цилиндр "является" кругом.

Клиент производного класса Cylinder легко осуществляет доступ к сервисам базового класса. Клиентская программа вызывает их (например, getLengthO), как если бы эти сервисы были определены в классе Cylinder. Структура класса Cylinder также несложная. В нем определены только те возможности, которые

604

Часть lil # Программшроваише с агрегированием и наследованием

отсутствуют в базовом классе Circle. Это так же просто, как и использование композиции с общедоступными элементами данных класса Circle. Несомненно, это легче, чем использование композиции с элементами данных, не объявленными общедоступными в классе Circle (как в листинге 14.2). Для композиции класса класс Cylinder должен реализовывать однострочный метод для каждого метода Circle. Он должен быть доступным для клиентской программы Cylinder. Для наследования подобные однострочники не используются.

Список функции инициализации для конструктора производного класса подоб­ ен списку функции инициализации для композиции класса — имя элемента набо­ ра данных в листинге 14.2 заменяется именем базового класса в листинге 14.3. Помните, что представляет собой список функции инициализации? Поскольку класс Circle не имеет конструктора, определенного по умолчанию, было бы син­ таксически неверно создавать объект класса Cylinder без списка функции инициа­ лизации, независимо от того, выполняется ли проектирование с использованием композиции или с использованием наследования.

Итак, проектирование с наследованием либо является настолько же сложным, как проектирование с композицией класса (например, для списков функции инициа­ лизации), либо более простым, чем композиция класса. (Однострочные элементы данных не используются.) Это не означает, что проектирование с использованием наследования лучше проектирования с применением композиции.

Листинг 14.3. Пример повторного использования программы посредством наследования

#include <iostream>

 

 

using

namespace std;

 

 

class

Circle

 

// исходный код для повторного использования

{ protected:

 

// одна изопций - наследование

double radius;

 

// внутренние данные

public-

 

// требуется инициализация

static const double PI;

public:

 

// конструктор преобразования

Circle (double r)

 

{

radius = r; }

 

 

double getLengthO

const

// вычисление длины окружности

{

return 2 * PI * radius; }

 

double getArea( )const

// вычисление площади

{

return PI * radius * radius }

 

double getRadiusO

const

 

{

return radius; }

 

 

void set(double r)

;

// изменение размера

{

radius = r; } }

const double Circle: : PI = 3.1415926536;

 

class Cylinder : public Circle

// новый класс Cylinder

{ protected:

 

// остальные данные в Circle

double height;

 

public:

 

// изCircle плюс новая программа

Cylinder (double r, double h)

 

: Circle(r)

 

// список функции инициализации (без PI)

{ height = h; }

 

// новая программа

double getVolumeO

const

// отсутствует getAreaO

{

return height * getAreaO; }

// дополнительные возможности

};

 

 

 

Глава 14 « Выбор между наследованием и композицией

605

i nt mainO

{

Cylinder су11(2.5,6.0), су12(5.О,7.5) ;

// инициализация данных

double

length = су11.getLength () ;

 

// подобно как в Circle

cyH.set (3.0) ;

 

// отсутствует вызов getAreaO

double diam = 2 * cyl1.getRadius();

 

double vol = cyl2.getVolume () ;

// отсутствует в Circle

cout «

" Circumference of first cylinder:

" « length « endl;

cout «

" Volume of the second cylinder: "

«

vol «

endl;

cout «

" Diameter of the first cylinder:

' «

diam « endl;

return 0;

 

 

 

}

 

 

 

 

 

Главная проблема использования наследования состоит в том, что у разработ­

 

чика клиентской программы отсутствует один сегмент, который описывает серви­

 

сы, предоставляемые серверным

классом. В листинге 14.2, где использовалась

 

композиция, этот сегмент программы был спецификацией самого класса Cylinder.

 

В листинге 14.3, где применялось наследование, спецификация класса Cylinder

 

описывает только то, что класс Cylinder добавляет к возможностям базового

 

класса Circle. Остальная часть сервисов, доступных для клиентской программы

 

класса Cylinder, описывается в спецификациях класса Circle. Программист кли­

 

ентской программы Cylinder обязан изучить сервисы, предоставляемые базовым

 

классом. Ситуация осложняется, если иерархия наследования велика.

 

Это не проблема для компилятора C+ + . Компилятор осуидествляет поиск по

 

дереву наследования для проверки законности сообщений в клиентской программе.

 

Так же поступают проектирова;ик и специалист по сопровождению. Но человеку

 

выполнить это намного сложнее, чем компилятору.

 

Вторая проблема наследования заключается в том, что проектировщик клиент­

 

ской программы должен достаточно хорошо изучить возможности базового класса

 

и перейти к использованию базовых сервисов, которые производный класс не

 

поддерживает. Например, клиентская программа может вычислять площадь по­

 

верхности цилиндра следующим образом:

 

 

double area = cyl1.getArea(

) ;

/ /

нонсенс - это не площадь поверхности!

 

Вызов приведенной функции выглядит так же, как вызовы методов getLength(),

getRadiusO и set(). Почему они являются одинаковыми для классов Circle и Cylinder, а метод getAreaO отличается от них? Эти вызовы выглядят одинаково для компилятора, для проектировщика клиентской программы Cylinder и для программиста, осуществляющего сопровождение, который может оказаться не­ достаточно компетентным в сложностях геометрии.

Проектировщик класса Cylinder обеспечивает возможность для клиентской программы Cylinder использовать методы getLength(), getRadiusO и set(), но не метод getArea(). Как достичь этого? Один из способов — использование за­ крытого или защищенного режима наследования.

class Cylinder : protected Circle { protected:

double height; public:

Cylinder (double r, double h)

:Circle(r)

{height = h; }

double getVolumeO const

{ return height * getAreaO; }

/ /

новый класс Cylinder

/ /

остальные данные в Circle

/ /

из Circle плюс новая программа

/ /

список функции инициализации

/ /

новая программа

/ /

отсутствует getAreaO

/ /

дополнительные возможности

606

Часть III # Програй^г^ирован11е с агретроваимвт т наследованием

Но это слишком много. Клиентская программа Cylinder не может вызвать getAreaO, потому что она защищена в классе Cylinder, но методы getLengthO, getRadiusO и set() также становятся защищенными. Есть два средства. Вы мо­ жете явно определить методы getLengthO, getRadiusO и set() как общедоступ­ ные в производном классе Cylinder, но при этом задавать метод getAreaO или какие-либо другие методы. Базовые сервисы должны быть закрыты от клиентов производного класса.

class Cylinder : protected Circle

/ /

новый класс Cylinder .

{ protected:

 

/ /

остальные данные в Circle

double height;

 

public:

 

/ /

getAreaO здесь отсутствует

Circle::getLength;

Circle::getRadius;

 

 

Circle::set;

 

 

 

public:

 

/ /

из Circle плюс новая программа

Cylinder (double r,

double h)

: Circle(r)

 

/ /

список функции инициализации (без PI)

{height = h; }

 

/ /

новая программа

double getVolumeO

const

/ /

без getAreaO

{ return height * getAreaO; }

/ /

дополнительные возможности

} ;

 

 

 

Это не так плохо, как кажется. Да, как-то неуклюже создавать общедоступные базовые методы, защищенные в производном классе, а затем снова объявлять их общедоступными. Но с точки зрения программной инженерии это превосходно. Вы получаете явный список общедоступных сервисов, которыми класс Cylinder обеспечивает своих клиентов.

Другое средство иллюстрируется в листинге 14.4. Производный класс Cylinder делает унаследованные методы явно доступными для клиентской программы Cylinder. Исключением являются методы (например, getAreaO), которые должны быть недоступны клиентской программе. Это очень похоже на версию с компо­ зицией класса в листинге 14.2. Обратите внимание на использование операции явного задания объекта класса внутри этих однострочных функций. В композиции класса это элемент данных, которому направляется сообщение. В наследовании отсутствует явный элемент данных. Вместо него существует базовая часть объекта производного класса. Пропуск операции явного задания объекта приводит к бес­ конечному рекурсивному вызову.

void Cylinder::set(double

г)

{ set(r); }

/ / неявный рекурсивный вызов

Вызов функции setO происходит в области действия класса Cylinder. В соот­ ветствии с правилом разрешения вызова функции, компилятор вначале осуществ­ ляет поиск имени, которое принадлежит локальной области действия. Поскольку это имя находится в классе Cylinder, компилятор снова вызывает Cylinder:: set() и никогда не поднимется по цепочке наследования до вызова метода Circle: :set(). Эта реализация эквивалентна следующей функции:

void Cylinder::set(double

г)

{ Cylinder::set(r); }

/ / явный рекурсивный вызов

Чтобы избежать бесконечной рекурсии, необходимо использовать операцию явного задания объекта Circle.

 

 

 

Глава 14 « Выбор между насдедовоние^л и композицией

607

Листинг 14.4. Пример повторного использования кода

 

 

 

 

 

 

посредством защищенного наследования

 

 

 

 

 

 

 

// исходный код для повторного использования

 

double radius;

// одна изопций - наследование

 

 

 

// внутренние данные

 

 

 

public-

 

// требуется инициализация

 

 

 

static const double PI;

 

 

 

public:

 

// конструктор преобразования

 

 

 

Circle (double r)

 

 

 

{

radius = r; }

 

 

 

 

 

 

double getLengthO const

// вычисление длины окружности

 

 

 

{

return 2 * PI * radius; }

 

 

 

 

 

 

double getArea () const

// вычисление площади

 

 

 

{

return PI * radius * radius; }

 

 

 

 

 

 

double getRadiusO const

 

 

 

 

 

 

{

return radius; }

 

 

 

 

 

 

void set(double r)

// изменение размера

 

 

 

' { radius = r; } };

 

 

 

const double Circle::PI = 3.1415926536;

// новый класс Cylinder

 

 

class Cylinder : protected Circle

 

 

{

protected:

// остальные данные в Circle

 

 

 

double height;

 

 

 

public:

 

// изCircle с добавлением новой

 

программы

 

Cylinder (double r, double h)

 

 

 

 

: Circle(r)

// список функции

инициализации

(без PI)

 

{ height = h; }

// новая программа

 

 

 

double getLength () const

// из класса Circle

 

 

 

{

return Circle::getLength(); }

 

 

 

double getRadiusO const

// из класса Circle

 

 

 

{

return Circle::getRadius(); }

 

 

 

 

 

 

void set(double r)

// из класса Circle

 

 

 

{ Circle::set(r) ; }

 

 

 

 

 

 

double getVolumeO const

// getAreaO отсутствует

 

 

 

{

return height * getAreaO; }

// дополнительная

возможность

 

 

 

} ;

 

 

 

 

 

 

 

int mainO

 

// инициализация

данных

 

 

{

Cylinder су11(2.5,6.0), cyl2(5.0,7.5);

 

 

 

double length = cyll .getLengthO;

// подобно как в Circle

 

 

 

cyl1.set(3.0);

// нет вызова getAreaO

 

 

 

double diam = 2 * cyl1.getRadiusO;

 

 

 

double vol = cyl2.getVolume();

// отсутствует в Circle

 

 

 

cout «

" Circumference of first cylinder: " «

length « endl;

 

 

 

cout «

"Volume ofthe second cylinder: " «

vol « endl;

 

 

 

 

cout «

" Diameter of the first cylinder: " «

diam « endl;

 

 

 

 

return 0;

 

 

 

 

 

# include

<iostream>

 

 

 

 

 

using

namespace std;

 

 

 

 

 

class

Circle

 

 

 

 

 

{ protected:

I 608

Часть III # Протраттырошаишв с третшроваитвт ш иасАВ^овантвт

Такое решение исключает оба недостатка использования наследования. Сундествует явный список сервисов, которые класс Cylinder передает своей клиентской программе. Отсутствует опасность того, что клиентская программа вызовет базо­ вые методы, использование которых является неподходяш,им для производного класса. С другой стороны, решение с использованием композиции, представлен­ ное в листинге 14.2, также не содержит этих двух недостатков. При возможности выбора предпочтительнее использовать композицию вместо заш,иш,енного насле­ дования, поскольку концептуально она прош,е, а связи между классами не на­ столько сильны, как в заш,иш.енном наследовании.

Наследование в повторно определенных функциях

Необходимость подавления некоторых базовых методов в объектах производ­ ных классов возникает только в том случае, если наследование используется ненадлежаидим образом. В этом случае показано, что объект производного класса не является объектом базового класса. Он владеет этим базовым объектом как элементом данных. По этой причине предпочтительнее использовать композицию вместо заш,иш,енного наследования.

Часто связь между классами достаточно близка к связи наследования, и в базо­ вом классе отсутствуют методы, которые должны подавляться. Однако должны быть методы, которые в производном классе интерпретируются иначе. Метод getArea() является хорошим примером. Для объекта базового класса Circle этот метод должен возвраш,ать плош,адь круга.

double Circle::getArea

() const

/ / вычисление площади круга

{ return PI * radius *

radius; }

 

Для объекта производного класса Cylinder подобный метод должен возврандать плоидадь двух кругов, которые содержит цилиндр и плош,адь боковой поверхности цилиндра.

double Cylinder::getArea

() const

/ / вычисление площади Cylinder

{ return 2 * Circle::PI *

radius * (radius

+ height); }

Когда метод производного класса скрывает метод базового класса, метод производного класса часто выполняет ту же работу, что и метод базового класса. Программисты C++ любят "документировать" этот факт явно, вызывая метод базового класса из метода производного класса (явно используя операцию для задания объекта базового класса).

double Cylinder::getArea () const

/ / вычисление площади Cylinder

{double area = Circle::getArea();

return 2 * (area + Circle::PI * radius * height); }

Переопределение базовых методов в производном классе — самая обычная практика программирования. Когда автор программировал на языке COBOL, его шеф рекомендовал использовать разные имена для каждой функции (или фраг­ мента текста), например COMPUTE-CIRCLE-AREA (вычисление плош.ади круга) и COMPUTE-CYLINDER-AREA(вычиcлeниe плош^ади цилиндра). К тому же тре­ бовалось использовать тш,ательно разработанную систему цифровых префиксов, которые указывали бы на то, какому модулю программы принадлежит каждое имя.

В языке C++ эта практика осуждается. Нельзя с уверенностью сказать, вызы­ вается ли требование использовать унифицированные имена (например, getArea()) только правилами хорошего тона. Техническое обоснование подобного подхода состоит в осуи;ествимости использования правил разрешения имен для определе­ ния того, какой метод (базовый или производный) должен быть вызван. Как видно из предыдундей главы, эти правила становятся частью интуитивного подхода к про­ граммированию.

Глава 14 • Выбор между наследованием и композицией

609

Окружность первого цилиндра: 15.708

Объем второго цилиндра:

589.049

Диаметр первого цилиндра: 6 Площадь первого цилиндра: 169.646

Рис. 14.2. Вывод для программы, представленной в листинге 14.5

Наследование с переопределением обычно является общедоступным. Часть проектов используется повторно (например, radius и getLengthO), добавляются новые ком­ поненты (например, height и getVolumeO), а некоторые методы переопределяются (например, getAreaO). Этот ва­ риант программы представлен в листинге 14.5. Клиентская программа Cylinder слегка изменена, чтобы продемонстри­ ровать использование метода getAreaO клиентской про­ граммой. Вывод программы показан на рис. 14.2.

Листинг 14.5. Пример повторного использования кода

 

посредством общедоступного наследования с переопределением метода

#include <iostream>

 

 

using namespace std;

 

 

class Circle

// исходный код для повторного использования

{ protected:

// наследование является одной изопций

double

radius;

// внутренние данные

 

public:

 

 

 

static

const double PI;

// необходимо инициализировать

 

public:

 

 

 

Circle (double r)

// конструктор преобразования

 

{ radius = r; }

 

 

double getLength () const

// вычисление окружности

 

{ return 2 * PI * radius; }

 

 

double getAreaO const

// вычисление площади

 

{ return PI * radius * radius; }

 

 

double getRadiusO const

 

 

{ return radius; }

 

 

void set(double r)

// изменение размера

 

{ radius = r; } };

 

const double Circle::PI = 3.1415926536;

 

 

class Cylinder : public Circle

// Действительно ли Cylinder является Circle?

{ protected:

// остальные данные определены

в Circle

double height;

public:

 

// изCircle плюс новая программа

Cylinder (double r, double h)

: Circle(r)

// список функции инициализации

(без PI)

{ height = h; }

// новая программа

 

double getAreaO const

// вычисление площади Cylinder

 

return 2 * Circle::PI * radius * (radius + height); }

 

double getVolumeO const

// дополнительная возможность

 

{ return height * getArea (); }

 

} :

 

 

 

int mainO

 

 

 

{

 

// инициализация данных

 

Cylinder cyll (2.5,6.0), cyl2 (5.0,7,5);

 

double length = cyl1.getLengthO;

// подобно Circle

 

cyl1.set(3.0);

 

 

double diam = 2 * cyll.getRadiusO;

 

 

Соседние файлы в предмете Программирование на C++