Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf310Часть 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)
Ос т о р о ж н о ! Передавайте параметры с соблюдением рекомендаций, приведенных в данной книге. Отклонение от этих рекомендаций упрощает написание программного кода, но скрывает от сопровождаюш,его приложение программиста намерения разработчика, т. е., какие параметры функции являются входными, а какие — выходными.
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; }
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 |
|
|
граммы |
|||
Глава 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.
