Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf520 |
Часть III • Програг^1М1ирование с агрегировониам и наследованиег^ |
Про возможности такую форму связи следует свести к минимуму, ограничив значение или переменную одним классом и исключив подобные коммуникации между классами. Впрочем, это не всегда реально, поскольку объектно-ориентиро ванная программа строится как набор взаимодействующих, а не полностью неза висимых классов. Следовательно, некоторые коммуникации между классами вполне законны и полезны. Тем не менее программист, работающий с C + -f, должен помнить о взаимодействии классов и стараться не использовать такую форму связности.
Если общую переменную нужно использовать в разных методах, принадлежа щих одному классу, применяйте коммуникации через элементы данных класса. Например, класс Sample предусматривает для каждого объекта класса память для хранения элемента данных value. На этой стадии изучения C-f + такое архи тектурное решение должно вызывать у вас беспокойство, но нужно иметь в виду, что оно поддерживает коммуникации между двумя методами Sample — set() и get(). Какое бы значение ни устанавливала функция set() (например, при вы зове set() в классе History), оно сохраняется с течением времени. Когда клиент класса Sample позднее вызывает функцию get() (например, в методах print () или averageO в классе History), функция get() получает значение, сохраненное в этом конкретном объекте Sample его методом set().
Элемент данных clata[] в классе History используется для коммуникаций моед^ функциями-членами History. Какое бы значение ни устанавливала функция set() класса History, функции print() и averageO будут применять именно его.
Кэтому можно прийти другими способами. Например, определить массив clate[ ]
вmain О как локальную переменную или глобальную переменную в файле и пере давать его методам класса History как параметр.
Посмотрите на следующую версию класса History. Она выполняет все те же функции, что и версия из листинга 12.4, но не хранит в качестве своего компо нента данных массив объектов Sample. Вместо этого класс History получает дан ные для работы от клиента main().
class |
History { |
|
|
|
|
|
|
|
|
|
|||
|
enum { size = 8 }; |
|
|
|
|
|
|
|
|||||
public: |
|
|
|
|
|
|
|
|
|
|
|
||
|
void set(Sample[]; double, int) const; |
|
/ / |
модификация значения |
|||||||||
|
void print (const Sample[]) const; |
|
|
/ / |
вывод предыстории |
||||||||
|
void |
average(const Sample[]) |
const; |
|
|
/ / |
вывод среднего значения |
||||||
|
} ; |
|
|
|
|
|
|
|
|
|
|
|
|
void History::set(Sample data[], |
double s, |
int i ) |
const |
|
|||||||||
{ |
data[i] . set(s); |
} |
|
|
|
|
|
/ / |
или просто: data[i] |
||||
void History::print |
(const |
Sample data[]) |
const |
/ / |
вывод предыстории |
||||||||
{ |
cout |
« |
"\n История измерений:" |
« endl |
« |
endl; |
|
|
|
||||
|
for |
(int |
i |
= 0; i |
< size; |
i++) |
|
|
|
/ / |
локальный |
индекс |
|
|
cout « |
" " « |
data[i] . get(); |
} |
|
|
|
|
|
||||
void History::average (const Sample data[]) |
const |
|
|
|
|||||||||
{ |
cout |
« |
"\n Среднее значение: "; |
|
|
|
/ / |
вывод среднего значения |
|||||
|
double sum = 0; |
|
|
|
|
|
|
/ / |
локальное |
значение |
|||
|
for |
(int |
i |
= 0; i |
< size; |
i++) |
|
|
|
/ / |
локальный |
индекс |
|
|
|
sum += data[i] . get(); |
|
|
|
|
|
|
|||||
|
cout |
« |
sum/size |
« |
endl; |
} |
|
|
|
|
|
|
|
Как бы ни была плоха данная архитектура, она синтаксически корректна и се мантически надежна. Ее недостаток — чрезмерные коммуникации между классом History и клиентом. Клиенту приходится поддерживать информацию, которая в листинге 12.4 использовалась классом History.
522 |
Часть Hi • Програ1У1Ш1ирование с агрегированием и наследованием |
Одним из важных вопросов является качество ПО. В данном варианте все не так плохо, как в случае использования для взаимодействия глобальных перемен ных других функций. Функция averageO применяет глобальные переменные (элементы данных) sum и i для взаимодействия с собой (на следующей итерации цикла), а не с другими функциями. Тем не менее такая конструкция свидетельству ет о низком качестве программирования, и ее следует избегать. Один из примеров потенциальных осложнений — желание использовать глобальные элементы дан ных для других целей (подобно использованию индекса в функции print(), чтобы избежать описаний переменных), а это часто ведет к конфликтам. С + + поддер живает следующую теорию разработчика ПО: "Пусть каждая функция использует свои отдельные локальные переменные и применяет их так, как считает необходи мым, без риска возникновения каких-либо конфликтов".
Нужно стремиться к тому, чтобы степень сопряжения отдельных фрагментов в программе была минимальной. Если этого можно добиться с помощью локаль ных переменных в одной компонентной функции, не следует "поднимать" такие переменные до уровня компонентных данных класса. Если нескольким компо нентным функциям одного класса нужно обращаться к одним и тем же данным, реализуйте их как элементы данных класса и не передавайте как параметры. Когда функции-члену нужны данные, определенные в другом классе, передавайте их через параметры и не применяйте глобальную переменную или общедоступные элементы данных другого класса.
С о в е т у е м Для взаимодействия отдельных сегментов программы C++ используйте связность через локальные переменные в методе.
Если для поддержки потока данных этого не достаточно, применяйте функции-члены класса. И только когда и их не хватает, передавайте информацию через параметры метода. В любом случае старайтесь не использовать глобальные переменные.
Применим эти принципы разработки ПО к архитектуре класса из листин га 12.4. Класс History — упрощенный контейнерный класс. Он не предусматри вает никакой защиты клиента от переполнения контейнера или от ссылки на несуществующий элемент массива. Данная версия контейнера имеет только во семь слотов (ячеек) для хранения объектов Sample. Несмотря на это ограничение, клиент в mainO помещает в контейнер девять значений. Конечно, компилятор такое поведение не волнует. Операционная система также выполняет программу без всяких проблем, хотя она некорректна (см. рис. 12.7). Это распространенная проблема для приложений, использующих контейнеры. Распределение обязаннос тей между клиентом и контейнерным классом может быть различным, но защита контейнера от переполнения должна быть реализована, что является обязанностью класса-контейнера, а не клиента.
Когда новое значение Sample помещается в контейнер, клиент в листинге 12.4 задает и само значение, и индекс, который будет использоваться для вставки. Между тем этот подход противоречит принципу разработки ПО, который состоит в переносе обязанностей в серверный класс (в данном случае — контейнер History). Возможно, для такого простого алгоритма это не важно (все вводимые значения поступают сразу, не прерывая операции объекта-контейнера), однако у клиента есть другие важные обязанности. Он не должен отслеживать, сколько места оста лось в контейнере. Мониторингом состояния контейнера должен заниматься сам объект-контейнер.
Скорее всего, интерфейс между клиентом и History: :set() прочно связаны. Клиент в дополнительном параметре вынужден передавать информацию об ин дексе. Согласно правилам взаимодействия классов, следующая по силе степень взаимодействия — коммуникации через элементы данных класса. Чтобы усовер шенствовать имеющуюся архитектуру, нужно хранить информацию об индексе
Глава 12 • Преимущества и недостатки составных к л а с с о в |
[ 523 | |
следующего объекта Sample в классе History, а не в клиенте. Необходимо коорди нировать работу программистов, если разделено то, что должно быть вместе.
Улучшенная архитектура контейнера представлена в листинге 12.5. Метод History: :set() с двумя параметрами заменен на метод History: :ас1с1() с един ственным параметром — значением, добавляемым в конец контейнера. Контей нер содержит один дополнительный элемент данных — индекс idx, позволяюш.ий отслеживать используемую контейнером память. Клиент не знает, полон контей нер или нет. Он просто передает мето/iy addO добавляемое значение.
Так как клиент теперь не следит за использованием памяти в контейнере, от слеживание занятой и доступной памяти и контроль за переполнением являются обязанностями контейнера. Соответственно, контейнер знает о структуре своей памяти и ограничениях. В версии, представленной в листинге 12.4, где клиент должен решить, куда поместить следуюш.ее значение, не было необходимости инициализировать контейнерный объект. В данной версии, где контейнер сам решает, куда попадает следуюш,ее значение, он должен инициализироваться пустой областью, что обеспечит попадание в первый слот первого значения. Сле довательно, класс History имеет используемый по умолчанию конструктор. В нем History устанавливает индекс idx в значение 0. В методе add() контейнерный класс проверяет, заполнен ли массив. Если есть свободное место, add() исполь зует еш.е один свободный слот и увеличивает значение индекса idx для ссылки на следуюидий свободный слот. Если с^юты для поступающих данных недоступны, метод add О ничего не делает и игнорирует запрос клиента.
Конечно, было бы хорошо сообш.ить клиенту, успешна ли попытка добавления значения Sample в History. Клиент смог бы инициировать некоторые меры вос становления или уведомить о ситуации пользователя программы. Однако про граммисту не стоит тратить на это время. Все массивы фиксированного размера следует применять лишь для быстрого макетирования, а после отладки алгоритма заменить их на динамические массивы (см. главу 6).
Результат программы из листинга 12.5 будет тем же, что и программы из лис тинга 12.4.
Листинг 12.5. Контейнерный класс с массивом фиксированного размера и ко>1троль за переполнением
#include <iostream> using namespace std;
class |
Sample { |
|
|
// класс компонента |
|
double value; |
|
|
// значение для примера |
||
public: |
|
|
|
// конструктор: поумолчанию и преобразования |
|
Sample (double x = 0) |
|
||||
{ |
value |
= x; |
} |
|
// метод-модификатор |
void set (double x) |
|
||||
{ |
value |
= x; |
} |
|
// метод-селектор |
double get |
() |
const |
|
||
{ |
return |
value; } } |
; |
|
|
class |
History |
{ |
|
|
// контейнерный класс |
enum { size = 8 }; |
|
// массив значений (фиксированного размера) |
|||
Sample data[size]; |
|
||||
int idx; |
|
|
|
// индекс текущего значения |
|
public: |
: idx(O) { |
} |
// массив первоначально пуст |
||
HistoryO |
|||||
void add(double); |
|
// добавление значения в конец |
|||
void print 0 const; |
|
// вывод предыстории |
|||
void averageO const; |
|
// вывод среднего значения |
|||
|
|
|
|
|
Глава 12 • Преимущества и недостатки составных классов |
527 |
|||||||||||||||
|
do { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cout |
« |
"" |
« |
h.getComponent().get(); |
|
|
|
|
|
|
|
|
||||||
|
|
} while |
(h.getNextO); |
|
|
|
|
|
/ / |
вычисление среднего значения |
|
||||||||||
|
h.averageO; |
|
|
|
|
|
|
|
|
|
|||||||||||
|
return 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Некоторые программисты предпочитают связывать методы-итераторы с отдель |
|||||||||||||||
|
|
|
|
|
ным итераторным классом и ассоциируют его с классом-контейнером. В данном |
||||||||||||||||
|
|
|
|
|
примере это не делается, чтобы не ус^южнять программу. Устраним свойственное |
||||||||||||||||
|
|
|
|
|
подобной архитектуре контейнера ограничение на число компонентов, которые он |
||||||||||||||||
|
|
|
|
|
может содержать. В предыдуш.их версиях контейнера его емкость была фиксиро |
||||||||||||||||
|
|
|
|
|
ванной и задавалась во время создания контейнера. Если клиент пытался помес |
||||||||||||||||
|
|
|
|
|
тить в контейнер больше элементов, чем тот мог содержать, то, увы, контейнер |
||||||||||||||||
|
|
|
|
|
с этим ничего не мог поделать. |
|
|
|
|
|
|
|
|||||||||
|
|
|
|
|
|
На самом деле проблема легко решается. Контейнерный класс должен выде |
|||||||||||||||
|
|
|
|
|
лить новое пространство, скопировать в него дополнительные данные, освободить |
||||||||||||||||
|
|
|
|
|
задействованную память и использовать новую область до тех пор, пока она не |
||||||||||||||||
|
|
|
|
|
будет исчерпана. Хорошей стратегией для выделения новой памяти является |
||||||||||||||||
|
|
|
|
|
удвоение ее объема (размера массива). |
|
|
|
|
|
|
||||||||||
|
|
|
|
|
void |
History: :add(clouble s) |
|
|
|
|
|
|
|
|
|||||||
|
|
|
|
|
{ |
i f |
(count |
== |
size) |
|
|
|
|
|
|
|
|
||||
|
|
|
|
|
|
{ size |
= size |
* 2; |
|
|
/ / |
удвоение |
размера, если нет памяти |
||||||||
|
|
|
|
|
|
|
Sample *p = new Sample[size]; |
|
|
|
|
|
|
||||||||
|
|
|
|
|
|
i f (p == NULL) |
|
|
|
|
|
|
|
|
|
||||||
|
|
|
|
|
|
|
{ |
cout |
« |
" Нет памяти\п"; |
exit(1); |
} |
|
|
/ / проверка на успех |
||||||
|
|
|
|
|
|
for |
(int |
i=0; |
i |
< count; |
i++) |
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
p [ i ] |
= data[i]; |
|
|
/ / |
копирование существующих |
элементов |
||||||||
|
|
|
|
|
|
delete |
[ |
] data; |
|
|
/ / |
удаление существующего массива |
|||||||||
|
|
|
|
|
|
data |
= р; |
|
|
|
|
|
|
/ / |
замена его на новый массив |
||||||
|
|
|
|
|
|
cout |
« |
" |
новый размер: " |
« size |
« endl; |
} |
|
/ / отладка |
|
||||||
|
|
|
|
|
|
data[count++].set(s); } |
|
|
|
|
|
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
/ / использование |
следующего доступного пространства |
|||||||
|
|
|
|
|
Чтобы алгоритм работал, элемент данных data должен обозначать динамически |
||||||||||||||||
|
|
|
|
|
распределяемый массив объектов Sample, что требует изменений в конструкторе. |
||||||||||||||||
|
|
|
|
|
class |
History |
{ |
|
|
|
|
/ / |
контейнерный |
класс: установка |
значения |
||||||
|
|
|
|
|
|
int size, |
count, |
idx; |
|
|
|
|
|
|
|
|
|||||
|
|
|
|
|
|
Sample |
*data; |
|
|
|
/ / |
динамическая |
память |
|
|||||||
|
|
|
|
|
|
public: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HistoryO |
: size(3), count(O) |
idx(O) |
|
|
/ / |
сделать массив пустым |
|||||||||
|
|
|
|
|
|
{ |
data |
= new Sample[size]; |
/ / |
выделение новой памяти |
|
||||||||||
|
|
|
|
|
|
|
i f |
|
(data |
== NULL) |
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
{ |
cout |
« |
" Нет памяти\п"; |
exit(1); |
} |
} |
|
|
||||
|
|
|
|
|
|
. |
. . } |
; |
|
|
|
|
|
/ / |
остальная |
часть класса History |
|||||
новый |
размер: 6 |
|
|
|
|
|
|
|
|
|
Данная версия контейнера показана в листинге 12.7. Для |
||||||||||
новый |
размер: 12 |
|
|
|
|
|
|
|
|
|
простоты примера в качестве начального размера контей |
||||||||||
История |
измерений: |
|
|
|
|
|
|
|
|
нера задается очень маленькое значение (три компонента). |
|||||||||||
3 |
5 |
7 |
11 |
13 |
17 |
19 |
23 29 |
|
|
|
|
Показана |
работа алгоритма. Результат программы проде |
||||||||
|
|
|
|
монстрирован на рис. 12.8. Сначала выводятся отладочные |
|||||||||||||||||
Среднее |
значение: 14.1111 |
|
|
|
|
|
|
||||||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
сообидения об увеличении размера контейнера с 3 до 6 (когда |
|||||||||
Рис. 12.8. |
|
|
|
|
|
|
|
|
|
|
в контейнер помендается четвертый элемент), а затем с б |
||||||||||
выполнения |
|
|
|
|
|
до 12 (когда в контейнере оказывается седьмое значение). |
|||||||||||||||
Результат |
|
|
|
|
|
||||||||||||||||
программы |
из лист^инга 12.7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||||
528 |
|
Часть IIU Программирование с огрегированиегм! и носдедованйег^! |
||||||
Листинг 12.7. Контейнерный класс с динамически распределяемой памятью |
||||||||
#inclucle <iostream> |
|
/ / |
динамический контейнер переменного размера |
|||||
using |
namespace std; |
|
|
|||||
class |
Sample { |
|
|
/ / |
класс компонента |
|||
double value; |
|
|
/ / |
значение для примера |
||||
public: |
|
|
|
|
|
|
||
Sample (double x = 0) |
/ / |
конструктор: по умолчанию и для преобразования |
||||||
|
{ |
value |
|
= x; |
} |
|
|
|
void set (double |
x) |
/ / |
метод-модификатор |
|||||
|
{ |
value |
|
= x; |
} |
|
|
|
double get |
|
() |
const |
/ / |
метод-селектор |
|||
|
{ |
return |
value; |
} } |
|
|
||
class |
History |
{ |
|
|
// контейнерный класс: установка значения |
|||
int |
size, |
count, |
idx; |
|
|
|||
Sample *data; |
|
|
|
|
||||
public: |
|
|
|
|
// сделать массив пустым |
|||
HistoryO |
: size(3), count(O), idx(O) |
|||||||
{ |
data = new Sample[size]; |
// выделение новой памяти |
||||||
|
if (data == NULL) |
|
} |
|||||
|
|
{ cout « |
" Нет памяти\п"; exit(1); } |
|||||
void add(double); |
|
// добавление значения в конец |
||||||
Sample& getComponentO |
// возвращает ссылку на Sample |
|||||||
{ |
return data[idx]; } |
// может быть, целью сообщения |
||||||
void getFirstO
{idx = 0; } bool getNextO
{return ++idx < count; }
void average () const; "HistoryO {delete [ ]data; }
} ;
void History::add(double s) |
|
|
|
||
{ |
if (count == size) |
|
// удвоение размера, если нет памяти |
||
|
{ size = size * 2; |
|
|||
|
Sample *p = new Sample[size]; |
|
|
||
|
if (p == NULL) |
|
} |
|
|
|
{ cout « " Нет памяти\п"; exit(1) |
// проверка науспех |
|||
|
for (int i=0; i < count; |
i++) |
// копирование существующих элементов |
||
|
p[i] = data[i]; |
|
|||
|
delete [ ] data; |
|
// освобождение существующей памяти |
||
|
data = p; |
« size « |
// замена на новый массив |
||
|
cout « " новые размеры: |
endl; } |
// отладочный вывод |
||
|
data[count++].set(s); } |
|
// использование следующей доступной области |
||
void History::average () const |
|
|
|||
{ |
cout « |
"\n Среднее значение: "; |
|
|
|
|
double sum = 0; |
i++) |
|
|
|
|
for (int i = 0; i < count; |
|
|
||
|
sum +=data[i].get(); |
|
|
|
|
|
cout « |
sum/count « endl; } |
|
|
|
int mainO
{ double a[] = {3, 5, 7, 11, 13, 17, 19, 23,^ 29 } ; // исходные данные History h;
for (int i=0; i < 9; i++) h.add(a[i]);
Глава 12 • Преимущества и недостатки составных классов |
529 3 |
||
cout « "\История измерений:" « endl; « |
endl; |
|
|
h.getFirstO; |
/ / |
перенос обязанностей |
|
do { |
/ / |
вывод каждого компонента |
|
cout « " " « h.getComponent().get(); |
|
||
} while (h.getNextO); |
|
|
|
h.averageO; |
|
|
|
return 0; |
|
|
|
} |
|
|
|
Обратите внимание, что при уничтожении объекта-контейнера динамическое управление памятью требует для деструктора возврата динамической памяти ис пользования.
Нетрудно придумать и более сложные конструкции: компоненты можно сорти ровать, искать в контейнере, удалять, вставлять, обновлять и сравнивать. Значи тельное число классов-контейнеров содержится в библиотеке Standard Template Library.
Вложенные классы
Вернемся к программе из листинга 12.5. Обратите внимание, что вместо до бавления функций-итераторов в данной версии программы клиент не использует
объекты Sample.
int mainO |
; |
//исходные данные |
{ double a[] = {35, 7, 11, 13, 17, 19, 23, 29 |
||
History h; |
// конструктор по умолчанию |
|
for (int 1=0; i < 9 i++) |
// доступно 8 слотов |
|
h.add(a[i]); |
// установка предыстории |
|
h.printO; |
// вывод предыстории |
|
h.averageO; |
// вычисление среднего |
|
return 0; } |
|
|
Вместо этого к классу Sample имеет доступ объект History. С+-1- позволяет
программисту определить серверный класс внутри клиентского класса. В резуль
тате имя вложенного класса будет невидимо вне составного,агрегатного класса.
class History { |
// невидим вне области действия клиента |
|
class Sample { |
||
double value; |
// закрытые данные: здесь они могут |
|
public: |
// быть общедоступными |
|
0) |
||
Sample (double х |
{value =х; } void set (double x)
{value =x; }
double show () const { return value; }
} |
|
// конец определения вложенного класса |
|
int size, count. idx; |
|
|
|
Sample *data; |
|
|
|
public: |
: size(3), count(O), idx(O) |
// сделать массив пустым |
|
HistoryO |
|||
{ data = new Sample[size]; |
|
// выделение новой памяти |
|
if (data == NULL) |
exit(l); } |
} |
|
{ cout « " Нет памяти\п" |
|||
. . . } |
; |
// остальная часть класса History |
|
