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

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

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

310Часть II * Объектно-ориентированное програл^г^ирование но C-t^-f

Влистинге 8.2 функция-сервер isLeap() имеет три параметра. Это не глобаль­ ные переменные. Переменные year, remainder и leap определяются в функцииклиенте main() как локальные. Почему это возможно? Потому что они не должны быть известны в области действия функции isLeapO, как в листинге 8.1. Вместо

этого функция isLeapO обращается к данным переменным как к фактическим аргументам — они передаются в вызове функции isLeap().

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

Как и в предыдущем примере, переменные year и remainder являются для функции isLeapO входными, а leap — выходная переменная. Откуда это из­ вестно? Достаточно взглянуть на заголовок (или прототип, если он используется) функции isLeapO, а не на тело функции:

void

isLeap(int year, int remainder, bool &leap)

/ / параметры

{ .

. . }

 

Можно ли сказать, не изучая тела функции, какова роль каждого параметра? Конечно. Параметры year и remainder передаются по значению. Следовательно, они не могут быть выходными параметрами, и функция isLeapO не может уста­ навливать их значение.

void isLeap(int

year, int remainder,

bool &leap)

/ / параметры

{ remainder=4;

year=2000; . . .

/ / бесполезно для значения параметров

Следовательно, можно сделать вывод, что это входные параметры. Значения фактических параметров должны устанавливаться в коде клиента перед вызовом функции, и эти значения будут использоваться функцией-сервером в вычислениях.

Аналогично параметр leap передается по ссылке. Это означает, что данный параметр выходной. На самом деле он может быть параметром ввода-вывода, т. е. функция-клиент может сначала устанавливать его значение, а функция-сервер — обновлять его. Но основная идея в том, что функция isLeapO изменяет значение параметра leap.

Как прийти к таким выводам? Для этого достаточно взглянуть на заголовок функции. Структурная диаграмма программы из листинга 8.2 показана на рис. 8.2. Она аналогична программе из листинга 8.1, но явный поток данных через глобаль­ ные переменные заменен на явный поток данных через параметры. Зависит ли по­ траченное время от размера и с/южности функции-клиента? Нет. А от сложности серверной функции? Нет. Переход от неявной связности к явной дает значитель­ ное уменьшение сложности исходного кода как с точки зрения разработчика, так и с точки зрения сопровождающего приложение программиста.

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

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

Глава 8 • Прогроммировоние с использование!^! функций

представление о потоке данных, исследовав лишь заголовки функции-сервера (или прототипы). Как говорится, почувствуйте разницу.

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

Однако такое снижение сложности не достигается автоматически, лишь благо­ даря использованию явного связывания через параметры вместо глобальных переменных. Следует корректно выбирать режимы передачи параметров. Рас­ смотрим, например, следуюшую версию функции-сервера isLeapO:

void

isLeap(int &year, int &remaincler, bool &leap)

/ / параметры

{

i f (remainder ! = 0)

 

 

leap

= false;

 

 

else i f

(year%100==0 && year%400!=0)

 

 

leap

= false;

 

 

else

 

 

 

leap

= true; }

 

Корректна ли она синтаксически? Да. А семантически? Да. Если использовать эту функцию вместо той, которая применяется в листинге 8.2, то результаты будут такие же, причем для любого набора входных данных.

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

Передавая в этой версии функции все параметры по ссылке, разработчик функции не может на этапе проектирования функции сообщить о том, что именно он задумал. Он знает, что параметр leap — единственный выходной параметр, но не может обозначить это в самом программном коде.

Сопровождающий приложение программист должен поверить, что передача по ссылке предусматривает изменение параметра функцией-сервером (если отсут­ ствует модификатор const), а передача по значению говорит о том, что параметр не изменяется. В противном случае ему придется изучать клиентскую и серверную функции во всех деталях, а не только просматривать список параметров сервера. В такой ситуации все преимущества явной связности сводятся на нет.

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

Если применение параметров настолько лучше, чем использование глобальных переменных, то почему же программисты до сих работают с глобальными пере­ менными? На то есть три причины.

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

312 Часть II ^ Объектно-ориентированн: : :,:огротьтровамтв ыаС^-^

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

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

Вторая причина применения глобальных переменных — производительность разработчика. Намного легче и быстрее написать серверную функцию, где исполь­ зуются глобальные переменные, а не параметры. При применении параметров (как в листинге 8.2) легко может оказаться, что введены дополнительные пара­ метры, в которых на самом деле нет необходимости, или наоборот, число парамет­ ров следует увеличить, что вынуждает возвраш^аться к функции и переписывать ее. Написание функции с параметрами связано с дополнительным временем на пред­ варительное планирование.

В листинге 8.1 необходимые для функции глобальные переменные определя­ ются и используются без дополнительного планирования. Когда-то это считалось важным преимуи;еством. Полагали, что ускорение разработки программного кода имеет критическое значение. Сегодня специалисты уже не считают, что облегче­ ние написания программного кода экономит время и деньги. Эта экономия дости­ гается за счет упрош.ения чтения программы, и современные языки, в том числе и C+ + , ориентированы на то, чтобы побуждать разработчика тратить больше времени на создание легко читаемого исходного кода.

Третья причина применения глобальных переменных для коммуникаций между функциями — недостаточные знания программистов. Они не особенно задумыва­ ются о сложности использования глобальных переменных в серверных функциях

и просто применяют их. Тем самым увеличивается необходимость взаимодействия

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

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

Использовать параметры функций лучше, чем глобальные переменные

Нужно передавать простые входные параметры по значению, а выходные — по ссылке

Следует передавать параметры-структуры и классы по ссылке, применяя для входных параметров модификатор const

Необходимо передавать выходные параметры,

используя модификатор const (и выходные массивы без const)

Ос т о р о ж н о ! Передавайте параметры с соблюдением рекомендаций, приведенных в данной книге. Отклонение от этих рекомендаций упрощает написание программного кода, но скрывает от сопровождаюш,его приложение программиста намерения разработчика, т. е., какие параметры функции являются входными, а какие — выходными.

// только один параметр

Глава 8 • Программирование с использованием функций

313

Как уменьшить степень связности

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

Как уменьшить поток данных между функциями? Непростая задача. Единст­ венный способ свести к минимуму зависимость между функциями состоит в их перепроектировании, т. е. разделении обязанностей между ними. Все другие подходы тш,етны.

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

Листинг 8.3. Пример объединения параметров в структуру

#inclucle <iostream> using namespace std; struct YearData

{ inf year, remainder; bool leap; } ;

void isLeap(YearData &data)

{ if (data remainder != 0) data.leap = false;

else if (datd.year%100-=0 && data year%400!=0) data.leap = false;

else

data, leap = true; }

int mainO

 

// локальная переменная

{ YearData data;

cout «

"Введите год: ";

// присваивание выходных полей

Gin »

data.year;

data, remainder = data.year % 4;

isLeap(data);

 

if (data.leap)

" високосный год\п";

cout «

data.year «

else

 

data,year «

" не високосный год\п";

cout «

return 0;

 

 

}

 

 

 

mainQ

data.year

data.remainder

data.leap

isLeapO

Рис. 8.3. Структурная диаграмма и поток данных

для программы из листинга 8.3

Число параметров здесь в самом деле меньше, чем в листин­ ге 8.2, но уменьшился ли поток данных между функциями? Поток данных для этой версии программы показан на рис. 8.3. Как видно, входных значений getVolume(cl) все равно два — data.year и data, remainder, а выходное значение одно — data. leap.

Можно даже сказать, что такую версию программы сложнее писать, она, определенно, труднее в понимании и ее сложнее повторно использовать, так как данную версию isLeapO нельзя применять без типа YearData. В любом случае основной вывод в том, что такая версия программы не уменьшает связности между

I 314 I Чость I! ^ Объвктно-ориеиттрованиое програштшрошаитв на СФ^»

функциями. Это естественно. При подготовке данной версии никакого перепроек­ тирования не выполнялось — в ней обязанности между функциями main() и isLeapO распределены точно так же, как в версии из листинга 8.2. Следова­ тельно, поток данных мещ1у ними остался тем же.

Некоторые программисты пытаются уменьшить связность, избегая выходных параметров, и считают, что для этого нужно применять возвращаемое функцией значение. И они в чем-то правы. В листинге 8.4 показана еще одна версия данной программы. В ней функция isLeapO возвращает значение, а не присваивает его выходному параметру leap.

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

#include <iostream> using namespace std;

bool

isLeap(int year,

int remainder)

/ /

меньше параметров

 

{

i f

(remainder !=

0)

 

 

 

 

 

 

return

= false;

 

 

 

 

 

else

i f

(year%100==0 && year%400!=0)

 

 

 

 

 

return

false;

 

 

 

 

 

 

else

 

 

 

 

 

 

 

 

 

 

 

return

true;

}

 

 

 

 

 

int mainO

 

 

 

 

 

 

 

 

{ int

year,

remainder;

 

/ /

локальная

переменная

(вход)

bool

leap;

 

 

 

 

/ /

локальная

переменная

(выход)

cout

«

"Введите год:

";

 

 

 

 

Gin »

 

year;

 

 

 

/ /

присваивание входных переменных

remainder = year % 4;

 

 

 

 

 

leap = isLeap(year,reminder);

 

 

 

 

i f

(leap)

year «

" високосный год\п";

 

 

 

 

cout «

 

 

 

else

 

 

year «

" не високосный

год\п";

 

 

 

 

cout «

 

 

 

return 0;

 

 

 

 

 

 

 

 

Здесь число параметров в потоке данных меньше, чем в листинге 8.2. Функция isLeapO в данном случае проще в написании, и нет необходимости бороться с параметром-ссылкой. Например, можно вовсе устранить переменную leap, не­ посредственно используя в операторе if функции main() возвращаемое функцией isLeapO значение, а не устанавливая сначала значение локальной переменной:

int mainO

remainder;

// нет переменной leap

{ int year,

cout «

"Введите

год: ";

// присваивание

входных переменных

cin »

year;

 

remainder = year % 4;

// используется

выходное значение

if (isLeap(year, remainder)==true)

cout «

year «

" високосный год\п";

 

else

 

year «

" не високосный

год\п";

 

cout «

 

return 0; }

isLeapC;
main()
Возвращаемое
значение
isLeapO
8 . 5 . Структ,урная диаграмма и потуск данных
для программы из листинга 8.5

Глава 8 • Программирование с использованием функций

I 315 I

Уменьшился ли в программе поток данных между функция­ ми mainO и isLeapO? Не совсем. Рис. 8.4 показывает поток дан­ ных для данной версии программы. Как видно, здесь все равно два входных значения — year и remainder, а выходное значение представлено значением, возвращаемым функцией.

Связность здесь также не уменьшилась, поскольку пере­ проектирование не выполнялось. Эта программа распределяет обязанности между функциями main() и isLeapO так же, как в версии из листинга 8.2.

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

mainQ

year remainder

Возвращаемое

значение

isLeapO

Рис. 8.4. Структурная диаграмма и поток данных

для программы из листинга 8.4

year

Рис.

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

Иногда такое ненужное разделение происходит в результате не­ понимания смысла параметра после исследования кода серверной функции без изучения клиента. Например, в листинге 8.4 смысл параметра remainder невозможно уяснить только из функции isLeap(). Программисту нужно исследовать к^шентскую функцию main() и сде­ лать вывод, что данная переменная представляет остаток от деления года на 4. Это значение используется в main() только как параметр

а потому имеет смысл скомбинировать вычисление remainder и вклю­ чить его в ту же функцию, в данном случае — в isLeapO.

В листинге 8.5 демонстрируется версия программы, в которой обязанности вычисления remainder перенесены из функции main() в серверную функцию isLeapO. Поток данных между функциями показан на рис. 8.5.

Действительно, теперь isLeap() получает у main() только одно значение и вы­ числяет remainder сама, не заставляя клиента делать это перед вызовом.

Листинг 8.5. Пример переноса обязанностей из клиента в функцию-сервер

#include <iostream> using namespace std;

bool

isLeap(int

year)

// еще меньше параметров

{ int

remainder=year%4;

// неследует разделять то, что должно быть вместе

i f

(remainder

! = 0)

 

 

 

return

false;

 

 

 

else i f

(year%100 == 0 && year%400

!=0)

 

 

return

false;

 

 

 

else

 

 

 

 

 

 

 

return

true;

}

 

 

int mainO

 

 

 

// локальная переменная

- нетremainder

{ int year;

 

 

год: ";

cout «

"Введите

// присваивание входных

переменных

Gin »

year;

 

 

316

Часть II # Объвктио-О:: • -

----.--/} •-: v::.; .". /- -. rr^iv-. .:.va но С+Ф

i f (isLeap(year))

cout « year « " високосный год\п"; else

cout « year « " не високосный год\п" return 0;

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

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

Кроме того, не нужно забывать об опасности избыточных коммуникаций между функциями. Лучший способ уменьшить связность — исключить необходимость коммуникаций, совместив те части, которые должны комбинироваться вместе.

Как далеко стоит при этом заходить? Имеет ли смысл переносить в isLeapO определение переменной year и вывод запроса пользователю? Это еш,е более уме­ ньшит поток данных между функциями, однако программистам потребуется согла­ совывать пользовательский интерфейс (какая функция за какую часть интерфейса отвечает), что проявится в уменьшении сцепления функции isLeap(): вычисления

вней будут скомбинированы с вводом-выводом.

Влистинге 8.5 функция main() отвечает за пользовательский интерфейс, а функ­ ция isLeapO — за вычисления. Разделение интерфейса с пользователем будет столь же нежелательно, как разделение вычислений. Обязанности ка>едой функции должны быть четко определены.

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

bool

isLeap(int year)

{

i f (year

% 4 | | year%100==0 && year%400!=0)

 

return

false;

 

else

 

 

return

true; }

Те, кто предпочитает компактный код, могут реализовать это таким образом:

bool

isLeap(int

year)

{

return (year

% 4 | | year%100-=0 && year%400) }

Как уже говорилось в главе 4, не факт, что эти усовершенствования стоят затраченных усилий, но в любом случае они не влияют на связность, поскольку не изменяют распределения обязанностей между функциями.

О с т о р о ж н о ! Часто степень связности увеличивается, когда разработчики включают в разные функции операции, которые должны реализовываться в одной функции. При этом увеличиваются коммуникации между разработчиками, затрудняется повторное использование функций

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

Глава 8 • Программирование с использованием функций

| 317 щ

Инкапсуляция данных

 

 

 

 

Как и в других языках, в C + +

программисты скрывают сложность компью­

 

 

 

терных алгоритмов в функциях. Каждая функция представляет собой набор опе­

 

 

 

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

 

 

 

отражает эту цель. Как правило, имя функции составляется из двух компонентов:

 

 

 

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

 

 

 

(или субъект) действия (например, processTransactionO). Когда объект действия

 

 

 

ясен из контекста, (например, когда он передается функции как параметр), можно

 

 

 

использовать только глагол (addO, deleteO и т.д.).

 

 

 

 

Набор операторов в функции может содержать простые операции присваива­

 

 

 

ния, сложные управляющие конструкции или вызовы других функций. Эти другие

 

 

 

функции могут быть библиотечными или определяемыми программистом функ­

 

 

 

циями, созданными для конкретного проекта.

 

 

 

 

С точки зрения программиста, разница между двумя видами функций состоит

 

 

 

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

 

 

 

можно проверить, а исходный код библиотечных функций — нет. Даже когда

 

 

 

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

 

 

 

клиентом, не захочет тратить время на их изучение. Ему нужно лишь описание

 

 

 

интерфейса серверной функции: какие выходные значения соответствуют входным

 

 

 

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

 

 

 

и исключения. Это позволяет программисту выбрать соответствующую библио­

 

 

 

течную функцию и корректно ее использовать.

 

 

 

 

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

 

 

 

раются. Исходный код этих функций часто модифицируется, чтобы он лучше

 

 

 

подходил под требования клиентских функций. Эти функции не протестированы

 

 

 

так хорошо, как библиотечные функции. Когда возникает проблема, ее источни­

 

 

 

ком может быть функция-клиент или любая из серверных функций. Следователь­

 

 

 

но,

программист, занимающийся функцией-клиентом (или сопровождающий ее),

 

 

 

должен изучить исходный код связанных с нею функций — клиентов и серверов.

 

 

 

Это усложняет задачу по сравнению с использованием библиотечных функций.

 

 

Желательно разрабатывать функции так, чтобы свести к минимуму подобные до­

 

 

 

полнительные сложности. Принцип инкапсуляции данных — один из принципов,

 

 

 

помогающих программисту достичь данной цели. После успешного тестирования

 

 

 

серверных функций они интерпретируются программистами, отвечающими за

 

 

 

клиентские и серверные функции, аналогично библиотечным функциям — как

 

 

 

"черный ящик" с известным интерфейсом.

 

 

 

 

Давайте рассмотрим простой пример — часть графического пакета, работаю­

 

 

щего с геометрическими фигурами, например цилиндрами. Для простоты пред­

 

 

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

 

 

типа double — радиусом и высотой цилиндра.

 

 

struct Cylinder

{

 

 

 

 

 

.double

radius,

height;

} ;

 

 

 

Данная программа запрашивает у пользователя размеры цилиндра. Если объем

 

 

первого цилиндра меньше объема второго, то она масштабирует первый цилиндр,

 

 

увеличивая его размеры на 20%, и выводит полученные размеры. В реальной

Введите

радиус

и высоту

первого

цилиндра:

50 40

ситуации такой код может быть частью програм­

мы,

которая использует объекты-цилиндры для

Введите

радиус

и высоту

второго

цилиндра:

70 40

описания процессов обмена, происходящих в хи­

Измененный размер первого

цилиндра

 

мическом реакторе, изучения электрического тока

радиус: 60высота: 48

 

 

 

 

в микропроцессоре или анализа стальной фермы.

Рис. 8.6. Результат

программы

 

В листинге 8.6 показан пример исходного кода,

 

а на рис. 8.6 — результат выполнения этой про-

 

из листинга

8.6

 

 

граммы

318

Часть II • Обьектно-ориентированное программирование но C-f-i-

Листинг 8.6. Пример прямого доступа к базовому представлению данных

#inclucle

<iostream>

 

 

 

/ /

пока нет инкапсуляции

using namespace std;

 

 

 

 

 

 

struct

Cylinder

{

 

 

 

/ /

структура данных для доступа

double

radius,

height;

} ;

 

 

 

 

int

mainO

 

 

 

 

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

Cylinder

c1, c2;

 

 

 

 

 

 

cout

«

"Введите радиус и высоту первого цилиндра:

/ /

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

первый цилиндр

cin

»

с1. radius »

с1.height;

 

cout

«

"Введите радиус и высоту второго цилиндра:

/ /

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

второй цилиндр

cin

»

с2.radius »

с2.height;

 

i f

(с1. height*c1. radius*c1.radius*3.141593

/ /

сравнение объемов

 

 

<

c2.height*c2.radius*c2.radius*3.141593)

/ /

масштабирование

 

{

c1. radius *= 1.2;

c1.height *= 1.2;

 

 

 

cout

«

"\пИзмененный размер первого

цилиндра\п

/ /

вывод нового размера

 

cout

«

"радиус: " «

с1.radius « "

высота: «

с1.height « endl; }

 

else

 

 

 

 

 

 

 

/ /

в противном случае ничего не делать

 

cout

« ДпРазмер первого цилиндра не изменен" «

endl;

 

 

return

0;

 

 

 

 

 

 

 

 

}

Здесь функция main() обращается непосредственно к представлению данных Cylinder и не прибегает для этого к помощи серверных функций. Она совмещает доступ к данным (например, с1. radius) с операциями с данными (такими, как вычисление объема, масштабирование размера или печать данных). В результате сопровождающий приложение программист должен уяснять смысл операций не по именам функций-серверов, а по исходному коду (что сложнее).

Конечно, разработчик программы может написать комментарии и пояснить смысл операторов, как это сделано в листинге 8.6, однако комментарии не всегда достаточно понятны для читателя и не всегда точны. Иногда их просто опускают. Еще хуже, когда у разработчика нет времени на обновление комментариев при изменении исходного кода.

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

int

mainO

/ /

перенос обязанностей на сервер

{

 

 

 

 

Cylinder с1, с2

/ /

данные программы

enterData(c1,"первого");

/ /

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

первого цилиндра

enterData(c2,"второго");

/ /

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

второго цилиндра

i f

(getVolunie(cl) < getVolume(c2))

/ /

сравнение объемов

 

{ scaleCylinder(c1,1.2);

/ /

масштабирование

 

printCylinder(cl); }

/ /

вывод нового размера

else

/ /

в противном случае ничего не делать

cout « "Нет изменений в размере первого цилиндра" « endl; return 0;

Глава 8 • Прогроммирование с использованием функций

Чтобы понять смысл данной версии функции main(), на самом деле нет необ­ ходимости разбираться в том, как серверные функции enterDataO, getVolumeO, scaleCylincler() и printCylincler() делают свою работу. Комментарии здесь те же, что и в листинге 8.6, где не используются функции доступа, но они здесь совсем не помогают, а просто повторяют то, что и так ясно из имен функций, вызываемых клиентом. Это одно из важных преимуш,еств "переноса обязанностей" с клиента на функции-серверы — принципа, о котором говорилось в начале главы.

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

Еще одна проблема со стилем программирования в том, что комбинирование доступа к данным с вычислениями их значений затрудняет и делает не очень по­ нятной проверку данных. Часто ее просто опускают. Например, в первой версии программы (листинг 8.6) никакой проверки данных нет. В этом примере данные поступают от пользователя, и следует заш^итить программу от ошибок. В реальной ситуации данные могут считываться из внешнего файла или поступать по комму­ никационной линии. Как и пользователь, эти источники нередко дают запорченные данные. Между тем даже простейшая защита от ошибок (например, присваивание полям Cylinder значений по умолчанию) усложняет код клиента:

i nt mainO

{Cylinder с1, с2;

cout « "Введите радиус и высоту первого цилиндра: ";

Gin »

с1. radius »

 

с1.height;

/ /

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

 

 

 

 

 

 

 

 

 

 

/ /

первый цилиндр

i f

(с1. radius

< 0)

с1. radius

= 10;

/ /

по умолчанию на случай

i f

(с1.height

< 0)

с1.height

= 20;

/ /

порчи данных

 

 

 

 

cout

«

"Введите радиус

и высоту второго цилиндра: ";

 

cin

»

с2.radius »

с2.height;

/ /

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

 

 

 

 

 

 

 

 

 

 

/ /

второй цилиндр

i f

(с2. radius

< 0)

с2. radius

= 10;

/ /

по умолчанию на случай

i f

(с2. height

< 0)

с2. height

= 20;

/ /

порчи данных

 

 

 

 

i f

(с1.height*c1. radius*c1.radius*3.141593

/ /

сравнить объемы

 

 

<

с2.height*c2.radius*c2.radius*3.141593)

 

 

 

{

c1. radius *= 1.2;

 

c1. height *= 1.2;

//масштабирование

 

cout

«

"\пИзмененный размер первого цилиндра\п";

 

 

 

 

 

 

 

 

 

 

 

 

/ /

вывод нового

размера

 

cout

«

"радиус:

"

«

с1. radius « " высота: "

«

с1. height «

endl; }

else

 

 

 

 

 

 

 

 

 

 

 

 

cout

«

"\пРазмер

первого цилиндра не изменен" «

endl;

 

return

0;

 

 

 

 

 

 

 

 

 

Использование функций доступа дает возможность устранить проверку данных в клиентском коде на нижнем уровне. Это нетрудно сделать, например, с помош,ью функции validateCylinderO, устанавливаюш,ей поля цилиндра в значения по умолчанию, если введены отрицательные числа. Данная версия программы пока­ зана в листинге 8.7. Результат ее будет таким же, как у версии из листинга 8.6.

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