Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfI |
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 + + допускает такое запутанное |
|||
|
|
|
сотрудничество. Более того, для некоторых программ такая архитектура может |
|||
|
|
|
быть полезной. Однако в результате это приводит к ненужному усложнению |
|||
|
|
|
структуры. Именно поэтому в большинстве случаев взаимодействия классов |
|||
|
|
|
имеется один класс, играющий роль клиентского класса, и существует другой |
|||
|
|
|
класс, играющий роль серверного класса. Когда метод клиентского класса отправ |
|||
|
|
|
ляет сообщение объекту серверного класса, говорится, что один класс "покупает |
|||
|
|
|
сервисы" другого класса. |
|
|
|
Существуют три ситуации, в которых клиентский метод может осуществить доступ к объекту-серверу и отправить ему сообщение:
•Определение объекта-сервера как локальной переменной в клиентском методе.
•Определение объекта-сервера как элемента данных в клиентском классе.
•Получение объекта-сервера как параметра клиентского метода.
Первая ситуация выгодна с точки зрения обмена сообщениями между класса ми: только одна клиентская функция (где определен объект-сервер) имеет доступ к объекту-серверу. Всегда следует выбирать такой тип связей "клиент-сервер". Часто это невозможно, поскольку доступ к объекту-серверу должен осуществляться с помощью других методов клиентского класса или других классов.
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 « Выбор между насдедовоние^л и композицией |
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()) только правилами хорошего тона. Техническое обоснование подобного подхода состоит в осуи;ествимости использования правил разрешения имен для определе ния того, какой метод (базовый или производный) должен быть вызван. Как видно из предыдундей главы, эти правила становятся частью интуитивного подхода к про граммированию.
