Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfГлава 7 • Программирование с использованием функций C-f+ |
301 |
Кроме того, в данной главе рассказывалось о присваивании параметрам значе ний по умолчанию и о перегрузке имен функций. Это превосходные средства язы ка, уменьшаюш^ие ограничения пространств имен в программных проектах C+ + . Они открывают также новые перспективы в сопровождении программ, позволяя обойтись без изменения существуюи;его клиентского кода, когда вызываемые из этого кода функции требуют изменений. Между тем данные средства должны ис пользоваться по возможности реже. Они сложны, и слишком многое происходит в программе C++ "за кулисами". Непродуманное использование данных возмож ностей может запутать не только компилятор, но и сопровождаюш,его ПО про граммиста.
Методы программирования функций составляют основу C+ + . Не освоив на хорошем уровне функции C+ + , невозможно создавать высококачественные объектно-ориентированные программы. На самом деле, без этого нельзя созда вать любые качественные программы — объектно-ориентированные или нет. Следуюш,ая глава начинается с изучения объектно-ориентированного программи рования — самого мош,ного способа создания высококачественных программ.
%Г^^6^
^
объектно-ориентированное программирование с использованием функций
Темы данной главы
•^ Сцепление
^Связность
у^ Инкапсуляция данных ^ Сокрытие информации
^ Большой пример инкапсуляции ^ Недостатки инкапсуляции в функциях ь^ Итоги
еэтой главы начинается обсуждение принципов и методов объектноориентированного программирования. Некоторые из них можно отнести к обычным навыкам программирования, другие сформулированы и адап
тированы для использования с C++ . Такие принципы и методы редко обсу>кдаются в других книгах по C+ + , поэтому даже программистам, уже имеюш^им опыт работы на C + + , не стоит пропускать данную главу.
В предыдуш,их главах основное внимание уделялось правилам языка C+ + , определяюш,им, что синтаксически допустимо и что недопустимо в языке C+ + . Подобно естественным языкам, недопустимые конструкции должны исключаться не только из соображений их неоднозначности или плохого стиля, а просто потому, что компилятор не сможет преобразовать их в объектный код. Что касается допус тимых конструкций, то они позволяют выразить одно и то же разными способами. В предшествующих главах сравнивались разные способы применения различных конструкций — часто с точки зрения корректности программы, ее производитель ности, и, конечно, стиля. Между тем основным вопросом было удобство сопровож дения программы — нужно добиться того, чтобы сопрово>едаюш,ий ее программист не тратил лишние усилия на попытки понять, что имел в виду разработчик, когда писал исходный код.
В этой главе (и в следуюш,их главах) понятность исходного кода также будет важным вопросом. Но фокус дискуссии сместится с написания управляюш,их кон струкций в исходном коде на более высокий уровень программирования: разбие ние программы на взаимодействуюш,ие части (функции и классы).
Глава 8 • Программирование с использованием функций |
303 |
Не будем углубляться в системный анализ и разбираться в том, какие функции должны присутствовать в приложении для достижения поставленных целей. Это чрезмерно расширило бы тему данной книги. Будем предполагать, что необходи мые функции для достижения целей программы уже имеются, и сконцентрируемся на способах использования дополнительных функций, улучшающих удобство со провождения и повторного использования программы.
Работу между функциями-клиентами, взаимодействующими для достижения це лей программы, всегда можно разделить несколькими способами. Есть также не сколько способов проектирования серверных функций, обрабатывающих данные и выполняющих операции по запросам функций-клиентов. Если предположить, что все версии эквивалентны с точки зрения корректности программы, то как выбрать лучшую?
Ранее большинство программистов в качестве критерия руководствовались производительностью программы. Прогресс в области аппаратного обеспечения сделал этот критерий неподходящим для большинства приложений, особенно для интерактивных. Для тех приложений, где производительность все еще важна, вы бираются влияющие на быстродействие алгоритмы и структуры данных, а не спо соб распределения работы между клиентскими и серверными функциями.
Еще один важный критерий — простота написания программного кода. Этот критерий до сих пор подходит и для небольших программ, разрабатываемых не сколькими программистами и используемых непродолжительное время (после чего они заменяются новыми), и для крупных систем, эксплуатируемых очень долго, в создании которых участвуют большие коллективы разработчиков. В то же время экономика разработки ПО предполагает в этих случаях другой подход. Лучшая версия программы — та, у которой отдельные части можно использовать повтор но и делать это легче (что предполагает экономию при разработке приложения и создания следующих версий), или та версия, которая проще в сопровождении (что предполагает экономию при развитии и совершенствовании программы).
Удобство сопровождения и повторного использования — это две наиболее важные характеристики качества ПО. Однако эти характеристики слишком общие. Вовсе не очевидно, какую версию кода легче и дешевле сопровождать, а какую проще использовать повторно.
Возможность повторного использования тесно связана с независимостью от дельных частей программы. Среди нескольких версий кода С4-+ версию, в кото рой разобраться проще и быстрее (предпочтительнее, не обращаясь к другим сегментам программы), как правило легче изменять без нежелательного влияния на другие фрагменты кода.
Таким образом, необходимость ссылаться на другие сегменты программы сви детельствует о плохом качестве кода, а возможность изолированного анализа исходного кода без ссылок на другие сегменты программы говорит о хорошем его качестве. Поэтому будем говорить, что одна версия кода лучше, чем другая, если она более понятна, т. е. чтобы разобраться в ней, требуется меньше усилий и об ращений к другим частям программы.
Все это хорошо, но для программиста-практика недостаточно специфично и точно. Концепции "понятности" и "независимости" должны поддерживаться более специфическими техническими критериями, которые легче распознавать и использовать. В данной главе предлагается несколько технических критериев. Два из них — сцепление и связность — относительно стары, а два других — инкапсуляция и сокрытие информации — довольно новы, и отрасль не накопила достаточно опыта их использования. Кроме инкапсуляции и сокрытия информа ции, будем использовать несколько разновидностей критериев, связанных с по нятностью и независимостью кода:
• Перенос обязанностей с функции-клиента на функцию-сервер
• Ограниченность знания, используемого клиентом и сервером
I 304 |
Часть II * Объвктио-орыешырошаииое протрать. '^;г414В НО C++ |
• Разделение задач клиентской и серверной функции
•Не разделение тех частей, которые должны быть вместе
•Передача знания разработчика сопровождаюидему приложение программисту в самой программе, а не в комментариях
Никакого всеохватывающего термина для этих принципов подобрать не удалось (принцип максимальной независимости?; принцип Штерна?; разделения знания по принципу необходимости?; принцип самодокументируемого кода?). Как будет понятно дальше, данные принципы в чем-то перекрещиваются друг с другом и с критериями сцепления, связности, сокрытия информации и инкапсуляции. Практикующие программисты должны быть знакомы со всеми перечисленными принципами. Их основное достоинство состоит в том, что все они применимы в работе и показывают, в каком направлении нужно двигаться, чтобы улучшить архитектуру программы и ее качество, как нужно усовершенствовать методы программирования.
В основе данных критериев лежит идея, что функции программы взаимодей ствуют друг с другом, выполняя части общей работы. Как бы ни распределялись между ними обязанности, функции должны использовать какие-то общие знания, иметь общие цели, работать над частью одной задачи. Все это производят разные, функции, но они — части одной программы. Чтобы сделать данные функции по нятными, чтобы их можно было повторно использовать, нужно так распределить между ними обязанности, спроектировать систему таким образом, чтобы зависи мости между функциями были минимальными.
Как это часто бывает, написание программы более высокого качества требует дополнительных усилий, а программа содержит больше строк, чем менее качест венная программа. Некоторые программисты (и менеджеры) будут, наверное, ра зочарованы таким увеличением объема работы. Но можно привести интересную аналогию с правилами дорожного движения.
Когда я стою на красном сигнале светофора, то иногда думаю, что без ограни чивающих правил дорожного движения добрался бы до места быстрее. Возможно, это и так, но не для всех мест назначения и не для всех водителей. Езда без правил приведет к авариям и пробкам на дорогах. Водители, избежавшие аварий и про бок, могут действительно добраться до места быстрее. Но многие другие попадут в пункт назначения значительно позже ожидаемого времени. Правила движения отнимают у нас время, чтобы, в конечном счете, сэкономить его.
Аналогично игнорирование правил удобства сопровождения и повторного ис пользования программы позволит написать ее быстрее, но так будет не для всех приложений и не для всех программистов. Время, сэкономленное на написании программы, будет существенно меньше времени, которое придется потратить, чтобы разобраться в ней и понять, каких целей стремились добиться разработчики (и где они ошиблись).
Вот почему в индустрии ПО столь большое внимание уделяется написанию комментариев. Комментарии в программе — это своего рода инвестиции, в конеч ном счете окупающие себя (когда они ясные, полные и не устаревшие). Между тем часто строки комментариев неполны, непонятны и не отражают изменений, вне сенных после написания программы. Затраты на написание понятного програм много кода предпочтительнее затрат на комментарии.
При написании небольшой программы правила создания качественного, по нятного кода не очень важны, но если разрабатывается большое приложение, то затраты на разработку качественного кода имеют решающее значение и в резуль тате дадут отдачу.
308 Часть!! * Объек^
Заметим, что перед вызовом функции isLeapO в функции main() входные пе ременные year и remainder должны иметь допустимые значения. Функция-клиент должна убедиться, что эти значения правильно инициализированы. Функциясервер isLeapO не проверяет допустимость значений. Она предполагает, что функция main О исполняет свои обязательства.
Аналогично выходные переменные (в данном случае leap) не обязаны содер жать допустимое значение перед вызовом функции-сервера isLeapO. Эта функ ция сама должна установить выходное значение, а клиент — позднее, после вызова (но не перед ним), его использовать.
Очень важно представлять поток данных между функциями. Если известно, что переменные year и remainder являются входными переменными функции isLeapO, то можно ожидать, что функция-сервер использует эти значения, но не изменяет их. Было бы крайне странно предполагать, что функция isLeap() делает что-то вроде следующего:
void |
isLeapO |
|
{ |
remainder = 4; year = 2000; ... |
/ / нонсенс! |
Кроме того, если известно, что переменная leap — выходная переменная функции isLeapO, то не стоит ожидать, что клиент main() инициализирует эту переменную перед вызовом isLeapO или будет изменять ее значение сразу после вызова, предварительно не использовав его для тех или иных целей.
int mainO |
|
|
||
{ cout |
« |
"Введите год: |
"; |
|
cin |
» |
year; |
/ / |
получение данных от пользователя |
remainder - year % 4; |
|
|
||
leap = false; |
|
|
||
isLeapO; |
/ / |
вводит в заблуждение (и некорректно), |
||
leap |
= true; |
|||
|
|
|
/ / |
если выполняется сразу после вызова |
Что будет думать сопровождающий приложение программист, прочитав при веденную выше функцию? После определения цели присваивания remainder (эта переменная используется в isLeapO для вычисления значения переменной leap), программисту придется снова исследовать функцию isLeapO и попытаться понять, для чего выполняется присваивание leap. Для маленькой функции достаточно не скольких секунд, чтобы сделать вывод: значение, присвоенное в клиенте main() переменной leap, не используется функцией-сервером isLeapO и даже самим клиентом main(). Но это лишь для маленькой функции. Для крупной программы потребуется гораздо больше времени. Сопровождающий ее программист может запутаться и сделать неверные выводы.
Действительно, некоторые программисты настолько не любят неинициализиро ванных переменных, что инициализируют их, даже когда в том нет необходимости. По их мнению, это помогает, когда функция-сервер по тем или иным причинам не присваивает значение. Однако isLeapO не относится к таким функциям! Как и большинство других функций. Если программисты понимают поток данных между функциями, то не возникает ситуация, когда функция не присваивает значе ния выходной переменной.
Как видно, такая невинная на первый взгляд "защитная" мера программиро вания дает в результате код, для понимания которого требуется больше времени. С точки зрения критерия качества (удобства чтения программы и независимости отдельных ее частей) эта техника неизбежно дает худший код, т. е. является пря мым вкладом в кризис ПО, который мы хотим преодолеть. Избегайте такой прак тики. Вместо инициализации всего подряд нужно сообщить сопровождающему
Глава 8 • Программирование с использованием функций |
309 |
приложение программисту, какие значения будут использоваться сервером в каче стве ввода (инициализируя их в клиенте), а какие являются выходными перемен ными сервера (не инициализируя их).
Надеюсь, вы следите за дискуссией и понимаете важность передачи сопровож дающему приложение программисту знания разработчика о потоке данных между функциями. Давайте вернемся к обсуждению связности.
Связность определяет, сколько усилий и времени потребуется для понимания потока данных между функциями. Часто для этого необходимо исследовать обра ботку данных клиентом и функцией-сервером. Например, в листинге 8.1 функция mainO присваивает значения переменным year и remainder, а isLeapO использует эти значения, а также что main() не инициализирует leap, isLeapO присваивает значение leap, а main() использует это значение после вызова1з1еар(). Все так.
Однако, чтобы выявить эти простые зависимости, надо изучить функцию-клиент и функцию-сервер во всей полноте. В таком тривиальном примере это сделать легко, но в более реалистичной и сложной функции значительного размера потре буется гораздо больше времени. Можно ли усовершенствовать данную трудоем кую и подверженную ошибкам технику? Конечно. С помощью явной связности.
Явная связность
Явная связность осуществляется через параметры функции: все переменные (вход и выход), используемые функцией-сервером, включаются в параметры этой функции, и глобальные переменные в потоке данных между клиентом и сервером не используются. Листинг 8.2 показывает тот же пример, что и в листинге 8.1, но неявный поток данных через глобальные переменные заменен на явные пара метры. Эта программа выполняется аналогично программе из листинга 8.1.
Листинг 8.2. Пример явного связывания через параметры
ttinclude |
<iostream> |
|
|
|
|
|
|
|||||
using |
namespace std; |
|
|
|
|
|
|
|||||
void |
isLeap(int |
year, |
int |
remainder, bool &leap) |
/ / |
параметры |
|
|
||||
/ / ввод: year, remainder; |
вывод: leap |
|
|
|
|
|||||||
{ |
i f |
(remainder |
!= |
0) |
|
|
|
|
|
|||
|
|
leap |
= false; |
|
|
|
|
|
|
|||
|
else |
i f |
(year%100==0 && year%400!=0) |
|
|
|
|
|||||
|
|
leap |
= false; |
|
|
|
|
|
|
|||
|
else |
|
|
|
|
|
|
|
|
|
|
|
|
|
leap |
= true; |
} |
|
|
|
|
|
|
||
int |
mainO |
|
|
|
|
|
|
|
|
|
||
{ int year, |
remainder; |
|
/ / |
локальные |
переменные |
(ввод) |
||||||
bool |
leap; |
|
|
|
|
|
/ / |
локальная |
переменная |
(выход) |
||
cout |
« |
"Введите |
год: "; |
|
|
|
|
|
||||
Gin » |
year; |
|
|
|
|
/ / |
получение данных от пользователя |
|||||
remainder = year % 4; |
|
|
|
|
|
|||||||
isLeapO (year, |
reminder, |
leap); |
|
|
|
|
||||||
i f |
(leap) |
|
|
|
|
|
|
|
|
|
||
cout |
« |
year |
« |
" |
високосный год\п"; |
|
|
|
|
|||
else |
|
|
|
|
|
|
|
|
|
|
|
|
cout |
« |
year |
« |
" |
не високосный год\п"; |
|
|
|
|
|||
return |
0; |
|
|
|
|
|
|
|
|
|
||
