Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf330 Часть II« Объектио-ориентировонное програтттроваитв но С^-^
от конкретных символов. Например, если нужно иметь дело с фигурными скобка ми, то алгоритм будет точно таким же. Между тем, если обрабатываемые прило жением выражения должны включать в себя фигурные скобки (или другие парные символы), функцию checkParenO следует изменить (наряду с другими функциями в приложении). Возможно, потребуется другое имя функции, посколь ку проверяться будут не только скобки.
Листинг 8.10. Пример прямого доступа к представлению данных
#inclucle |
<iostream> |
|
|
|
/ / |
пока что нет инкапсуляции |
||||||||
#inclucle |
<cstring> |
|
|
|
|
|
|
|||||||
using namespace std; |
|
|
|
|
|
|||||||||
char |
buffer[81]; |
char |
store[81]; |
|
/ / |
глобальные данные |
||||||||
bool |
checkParen |
() |
|
|
|
|
|
|
||||||
{ char c, |
|
sym; |
int |
i , |
idx; |
bool valid; |
|
|
||||||
i |
= 0; |
idx |
= 0; |
valid— |
true; |
|
/ / |
инициализировать данные |
||||||
while |
( b u f f e r [ i ] |
!= |
'\0' |
&& valid) |
/ / |
конец данных или ошибка? |
||||||||
{ |
с = |
b u f f e r [ i ] ; |
|
|
|
|
/ / |
получить следующий символ |
||||||
|
i f |
(c=='(' |
II |
C=='[') |
|
|
/ / |
следующая скобка - открывающая? |
||||||
|
|
{ |
store[idx] |
= c; idx++; } |
|
/ / |
затем сохранить ее |
|||||||
|
else |
i f |
(c=='(' |
II |
c==']') |
|
/ / |
следующая - закрывающая? |
||||||
|
|
i f |
(idx |
> 0) |
|
|
|
|
/ / |
существует ли сохраненный символ? |
||||
|
|
{ |
idx-; |
sym = store[idx]; |
|
/ / |
получить последний символ |
|||||||
|
|
|
i f |
|
(!((sym=='(' |
&& c==')') |
II |
/ / |
если непарные |
|||||
|
|
|
|
|
|
(sym=='[' |
&&c==']'))) |
|||||||
|
|
|
|
|
valid |
= false; |
} |
|
/ / |
тогда ошибка |
||||
|
|
else |
|
|
|
|
|
|
|
|
/ / |
если нет парного сохраненного символа, ошибка |
||
|
|
|
valid |
= false; |
|
|
|
|||||||
|
i++: |
} |
|
|
|
|
|
|
|
|
|
/ / |
перейти к следующему символу |
|
|
i f |
(idx |
> 0) |
valid = false; |
|
/ / |
непарная левая скобка - ошибка |
|||||||
|
return |
|
valid; |
} |
|
|
|
|
/ / |
возврат статуса ошибки |
||||
void |
checkParenTest(char |
expression[]) |
/ / |
тестирующая функция |
||||||||||
{ Strcpy(buffer.expression); |
endl; |
|
|
|||||||||||
cout « |
|
"Выражение " « |
buffer « |
/ / |
вывод выражения |
|||||||||
if (checkParenO) |
|
|
|
/ / |
проверить допустимость |
|||||||||
|
cout « |
"допустим\п"; |
|
/ / |
вывод результата |
|||||||||
else |
|
|
|
|
|
|
|
|
|
|
|
|
||
|
cout |
« |
"недопустимо\п"; |
|
|
|
||||||||
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int |
mainO |
|
|
|
|
|
|
|
|
/ / |
инициализатор тестов |
|||
{ |
checkParenTest("a=(x[i]+5)*y;"); |
|
/ / |
первый тест: допустимое выражение |
||||||||||
checkParenTest("a=(x[i)+5]*y;"); |
|
/ / |
второй тест: недопустимое выражение |
|||||||||||
return |
0; |
|
|
|
|
|
|
|
|
|
||||
Серверные функции должны инкапсулировать детали (вид символов и правила поиска парных символов) от клиента. Вот пример трех функций доступа, которые выполняют эту работу. Индекс в массиве символов buffer[ ] передается функциям isLeftO и isRightO, возвращающим true или false в зависимости от того, на какой символ указывает индекс. Для функции symbolsMatch() передаются два ин декса — в массиве buffer[] и в массиве store[], а функция возвращает true или false в зависимости от того, совпадают указываемые этими индексами символы или нет.
|
|
|
Глава 8 • Прогром1^^ирование с использованием^ функций |
I 333 |
||
|
while (buffer[i] != АО' &&valid) |
/ / |
конец данных или ошибка? |
|
||
|
{ с = buffer[i]; |
/ / |
получить следующий символ |
|
||
|
if (isLeft(c)) |
/ / |
следующая скобка - открывающая? |
|||
|
{ storeCidx] = c; idx++; } |
/ / |
затем сохранить ее |
|
||
|
else if (isRight(c)) |
/ / |
следующая - закрывающая? |
|
||
|
if (idx > 0) |
/ / |
существует ли сохраненный символ? |
|||
|
|
{ sym = store[-idx]; |
V / |
получить последний символ |
|
|
|
|
if (!(symbolsMatch(c,sym)) |
/ / |
если непарные |
|
|
|
else |
valid = false; } |
/ / |
тогда ошибка |
|
|
|
|
|
|
|
|
|
|
valid = false; |
/ / если нет парного сохраненного символа, ошибка |
||||
|
i++; } |
|
|
/ / |
перейти к следующему символу |
|
|
if (idx > 0) valid = false; |
/ / |
непарная левая скобка - ошибка |
|||
|
return valid; } |
/ / |
возврат статуса ошибки |
|
||
void checkParenTest(char expression[]) |
|
|
|
|||
{ |
cout « |
"Выражение " « Expression « endl; |
/ / |
вывод выражения |
|
|
|
if (checkParen(expression)) |
/ / |
проверить допустимость |
|
||
else |
cout « |
"допустимо\п"; |
/ / |
вывод результата |
|
|
cout « |
"недопустимо\п";} |
|
|
|
||
|
|
|
|
|
||
int |
mainO |
|
|
|
|
|
{ |
|
|
|
/ / |
первый тест: допустимое выражение |
|
checkParenTest("a=(x[i]+5)*y;"); |
||||||
checkParenTest("a=(x[i)+5]*y;"); |
/ / |
второй тест: недопустимое |
выражение |
|||
checkParenTest("a=(x(i]+5]*y;' |
/ / |
третий тест: недопустимое |
выражение |
|||
return |
0; |
|
|
|
|
|
В данной версии программы инкапсуляция намного лучше, а разделение обя занностей более согласованно. Клиент знает о массивах и индексах, а серверные функции — о символах и правилах их сопоставления.
Знание об одном из массивов, buffer[ ], для клиента естественно. Это массив, обрабатываемый checkParen(). Его инкапсуляция особого смысла не имеет. Если обработка выражения выполняется поэтапно, то функция checkParen() будет од ной из функций доступа, осуидествляющих проверку допустимости и обработку выражения.
Между тем checkParenO использует и другой массив — store[]. Этот массив усложняет исходный код. Программист должен решить, инициализировать ли idx нулем или каким-то другим значением. Когда символ сохраняется в массиве,, программисту приходится сначала решать, нужно ли сохранять первый символ, а затем увеличивать индекс. При считывании символа из массива следует опреде лить, нужно ли сначала получить символ, а потом увеличить индекс, или делать это каким-то другим способом. (Надо отметить, что ответы на два последних во проса различны.) Кроме того, когда функция checkParen() проверяет, остались ли
вмассиве store[] непарные символы, приходится решать, сравнивать ли индекс
снулем, единицей или каким-то другим значением.
Ответить на эти вопросы несложно, так как программа невелика, однако в со четании с другими вопросами все становится труднее, увеличивается вероятность ошибки на этапе разработки и особенно на этапе сопровождения. Еш,е важнее, что эти проблемы имеют мало обш,его с алгоритмом, реализуюил^им checkParen() — просмотром символов, сохранением левых скобок и их извлечением при обнару жении правой скобки. Каждая функция должна работать только с одной неинкапсулированной структурой данных, и для checkParenO такой структурой является массив buffer[], а не store[].
|
|
|
Глава 8 • Программирование с использованием функций |
|
335 |
||||
bool symbolsMatch |
(char с, |
char sym) |
// совпадают ли они? |
|
|
||||
{ |
return |
(syin=='('&&c==')')| |(sym=='['&&c==']'); |
|
|
|||||
bool checkParen (char buffer[]) |
// выражение - параметр |
|
|
||||||
{ |
Store store; |
|
|
// массив инкапсулирован |
|
|
|||
|
char csym; |
int i; bool valid; |
// инициализировать данные |
|
|
||||
|
i = 0; initStore(store); void = true; |
|
|
||||||
|
while (buffer[i] != '\0' |
&& valid) |
// конец данных или ошибка? |
|
|
||||
|
{ с = buffer[i]; |
|
// получить следующий символ |
|
|||||
|
if (isLeft(c)) |
|
// следующая скобка - открывающая? |
||||||
|
|
{ saveSymbol(store,c); } |
// затем сохранить ее |
|
|
||||
|
else if (isRight(c)) |
|
// следующая - закрывающая? |
символ? |
|||||
|
if (! isEmpty(store)) |
// существует ли сохраненный |
|||||||
|
{ sym = getLast(store); |
// получить последний символ |
|
||||||
|
if (! (symbolsMatch(c,sym)) |
// если непарные |
|
|
|||||
|
else |
valid = false; } |
// тогда ошибка |
|
|
||||
|
|
|
|
|
|
|
|
||
|
valid = false; |
/ / |
если нет парного сохраненного символа, ошибка |
||||||
|
i++; } |
|
|
|
/ / |
перейти к следующему символу |
|||
|
if (store.idx>0) valid=false; |
/ / |
непарная левая скобка - |
ошибка |
|||||
|
return valid; } |
|
/ / |
возврат статуса ошибки |
|
|
|||
void checkParenTest(char expression[]) |
/ / |
тестирующая функция |
|
|
|||||
{ |
cout « |
"Выражение " <<expression « endl; |
/ / |
вывод выражения |
|
|
|||
|
if (checkParen(expression)) |
/ / |
проверить допустимость |
|
|
||||
|
else |
cout « |
"допустимо\п"; |
/ / |
вывод результата |
|
|
||
|
cout |
« |
"недопустимо\п";} |
|
|
|
|
||
|
|
|
|
|
|
||||
int mainO |
|
|
|
|
|
|
|
||
{ |
cout « |
endl |
« |
end1; |
|
|
|
|
|
|
checkParenTest("a=(x[i]+5)*y;"' |
/ / |
первый тест: допустимое |
выражение |
|||||
|
checkParenTest("a=(x[i)+5]*y;' |
/ / |
второй тест: недопустимое |
выражение |
|||||
|
checkParenTest("a=(x(i]+5]*y;' |
/ / |
третий тест: недопустимое |
выражение |
|||||
|
cout « |
endl |
« |
end1; |
|
|
|
|
|
|
return |
0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
В данной версии программы отсутствуют детали операций сданными, мешаю щие понимать смысл действий, отвлекаюндие внимание разработчика и сопровож дающего приложение программиста. Область внимания разделяется на четыре узких зоны, а клиент и сервер не обмениваются знанием о структурах данных. Обязанности доступа к данным переданы функциям-серверам.
Недостатки инкапсуляции с использованием функций
Это превосходный способ разработки ПО. Но в реализации инкапсуляции и сокрытия информации с помощью одних лишь функций есть ряд недостатков. Данные недостатки создатели C++ постарались преодолеть с помощью классов.
Один из недостатков состоит в том, что функции доступа не сообщают про граммисту то, что задумывал разработчик, а именно — что функции имеют отно шение друг к другу и обращаются к одной структуре данных. Лучшее решение состоит в том, чтобы поместить функции isLeftO, isRightO и symbolsMatch() (функции доступа к символам) в один файл, а функции initStoreO, isEmptyO, saveSymbolO и get Last () (функции, обращающиеся к временному хранилищу) — в другой.
336Часть И • Объектно-ориентированное прогро^лг^ировоние но C-f-t-
Вреальности функции, обращающиеся к одной структуре данных, часто euuiviLщаются с функциями, работающими с другими структурами. Они размещаются
вфайле по алфавиту, и соотношение между структурами данных и функциями доступа становится неясным. Даже когда родственные функции помещаются в от дельный файл без каких-либо дополнительных функций, такое решение все равно остается "управленческим", а не поддерживаемым языком. В языке С (и в некото рых более ранних) отсутствовал какой-либо механизм указания, что некоторые функции логически связаны друг с другом, но не с другими функциями. C + + пред лагает превосходное решение. Он позволяет связывать данные и относящиеся
кним функции доступа в классы (ограничивая всю конструкцию фигурными скобками). Сами границы класса показывают, что функции и данные соотносятся друг с другом и не могут группироваться с другими несвязанными функциями.
Второй недостаток инкапсуляции с помощью функций заключается в ее произ вольности. Разработчик клиентской части может использовать функции доступа или отказаться от них, обращаясь непосредственно к полям структуры. Правила языка этого не запрещают. Например, в конце функции checkParenO в листин ге 8.13 проверяется, осталась ли в store[] открывающая скобка, для которой при вызове CheckParenO не оказалось парной скобки. Для корректности с этой целью нужно было бы использовать функцию isEmptyO:
i f (!isEmpty(store)) valid=false; |
/ / ошибка: непарная открывающая скобка |
Вместо этого для краткости было использовано имя поля idx структуры опреде ленного программистом типа Store:
i f (store. iclx>0) valid=false; / / ошибка: непарная открывающая скобка
Все преимущества инкапсуляции сведены на нет. Исходный код не говорит сам за себя — смысл нужно уяснять из контекста и комментариев. Задача сопровож дающего приложение программиста усложняется необходимостью иметь дело с комбинацией доступа к данным и операций с данными. Если нужно изменить имя поля данных idx, например на top (более распространенное), потребуется модифицировать и код клиента. Такие зависимости между клиентом и сервером усложняют программу. Именно поэтому не очень хорошо полагаться на благора зумие программиста и считать, что он обойдется с инкапсуляцией данных наилуч шим образом. C + + решает проблему, предоставляя разработчику квалификатор доступа private, который делает нарушение инкапсуляции невозможным.
Третий недостаток в том, что функции доступа являются глобальными. Их имена — часть глобального пространства имен, отсюда они могут конфликтовать с именами других функций. Следовательно, программисты, работающие над раз ными частями программы, должны координировать свои действия, чтобы избе жать конфликтов имен. Кроме того, это вынуждает программистов разбираться в других частях программы больше, чем необходимо.
C++ разрешает проблему, вводя в дополнение к области действия блока, функции, файла и программы область действия класса. Каждое имя, определенное как компонент класса, находится в области действия этого класса. Тем самым устраняются конфликты имен. Программисту не нужно знать об именах, исполь зуемых в других частях программы, если он с ними не работает. Тем самым уменьшается необходимость координации между программистами.
Другим недостатком является требование инициализации многочисленных структур данных в клиенте. Например, переменная store в листинге 8.13 ини циализируется явным вызовом функции initStoreO. В результате расширяется область, которой должен уделять внимание сопровождающий приложение про граммист, создается возможность использования данных без должной инициали зации.
Глава 8 • Программирование с использованием функций |
337 |
в C + + проблема устраняется переносом обязанностей с клиента в специаль ные серверные функции — конструкторы. Они неявно вызываются при каждом создании объекта класса. В этой функции разработчик класса сервера задает, как должен инициализироваться объект класса. В процессе разделения обязанностей между клиентом и сервером работа передается серверу, за эту инициализацию отвечает программист, занимаюидийся сервером, а программист, работающий с клиентской частью, от нее освобождается. Кроме того, C + + предусматривает другой тип специальных функций — деструкторы. Они неявно вызываются при уничтожении объекта класса. Эти функции возвращают динамическую память и другие ресурсы, которые получил объект, освобождая от таких действий про граммиста клиентской части.
В C + + есть еще ряд методов, способствующих связыванию данных и опера ций, инкапсуляции имен полей сервера, сокрытия архитектуры сервера от клиента, переноса обязанностей с клиентов на серверы, минимизации зависимости между клиентской и серверной частью.
Классы C + + обладают огромным потенциалом повышения качества ПО. По дробнее они будут обсуждены в следующих главах.
Итоги
В данной главе рассмотрено использование функций С+Н основного ин струмента программирования. Функциональность программы можно реализовать в C + + многими способами.
Цель переноса обязанностей на функции состоит в том, чтобы получить про грамму, функции которой понятны и легко сопровождаемы, изолированы от других функций и легко используются в разных контекстах. Все, что требует от разработчика клиента (или сопровождающего его программиста) чтения разных фрагментов в разных частях программы для ее понимания и модификации, пре пятствует повторному использованию программных компонентов и затрудняет сопровождение.
Критерии читабельности и независимости компонентов слишком общие. Для практики необходимы более конкретные критерии, поддерживаемые конкретной технической частью. В данной главе обсуждались традиционные критерии сцепле ния и связности, а также объектно-ориентированные критерии — инкапсуляция и сокрытие информации. Рассказывалось также о новых критериях, таких, как перенос обязанностей с клиентских функций на серверные, предотвращение раз деления связанных функциональных частей, проблемы разделения и ограничения общей для компонентов информации, а также о том, каким способом (отличным от комментариев) разработчик может сообщить в программе о своих замыслах.
Сцепление описывает, насколько хорошо соотносятся друг с другом элементы функции. Функции с сильным сцеплением делают что-то одно с одним объектом. Функции со слабым сцеплением делают несколько вещей. Избавиться от слабого сцепления можно перепроектированием функции — включением разных опера ций в разные функции, а не совмещением их в одной. Сцепление — не очень строгий критерий, его следует использовать как дополнение к другим.
Связность описывает интерфейс между функцией-сервером и ее клиентскими функциями. Слабая связность означает, что функции относительно независимы. Самая сильная форма связности состоит в использовании глобальных перемен ных. Это требует координации между разработчиками, занимающимися клиент скими и серверными функциями. Когда функции применяются в других контекстах, используются те же имена переменных. Чтобы проанализировать поток данных между функциями, придется изучить весь исходный код клиентских и серверных функций.
338 I Часть II t Объектно-ориентированное програтмтроваитв на С^-^-
Если для коммуникаций между функциями применяются параметры, эти функ ции легче использовать повторно. Разработчикам нужно координировать число и типы параметров, но не их имена. Поток данных можно понять, изучив лишь заголовки функций, а не весь код. Чтобы извлечь преимущества из такого под хода, разработчикам следует помнить рекомендации для передачи параметров, о которых рассказывалось в данной и предыдущих главах.
Для уменьшения связности следует так перераспределить обязанности между функциями, чтобы переместить операции, выполняемые в разных функциях, в одну. Тем самым устранится потребность в дополнительных коммуникациях между функциями. Разработчики всегда должны следить за тем, какие коммуника ции действительно необходимы, а каких следует избегать. Это очень важный инст румент в наборе программиста.
Инкапсуляция — метод программирования, изолирующий клиентские функции от имен и полей данных, которые этим клиентам нужны. К таким полям по запросу клиентов обращаются функции-серверы. В клиенте используются вызовы сервер ных функций, а не обращения к полям структур данных. Тем самым программа становится более сопровождаемой, так как создаются две независимые области. При изменении архитектуры программы изменяются функции доступа, а клиент ская часть остается той же. При изменении функций приложения изменяется клиентская часть, а функции доступа сохраняются. Если инкапсуляция не преду сматривается, то придется проверять на возможные изменения все фрагменты исходного кода.
Сокрытие информации — это метод программирования, еще более изолирую щий функции-клиенты от представления данных. Функции доступа выбираются так, чтобы выполнять операции от имени клиентских функций. Программный код клиента выражается в терминах вызовов серверных функций, имена которых описывают алгоритм клиента. Подобный подход еще более улучшает удобство сопровождения и повторное использование.
Если эти методы применять продуманно и последовательно, клиентский код станет объектно-ориентированным, так как будет выражаться в терминах опера ций со структурами данных. Однако объектно-ориентированное программирова ние с использованием функций не решает некоторых вопросов. На уровне языка никак не сказывается, что данные и их функции доступа как-то связаны. При со провождении программы приходится догадываться об этом, изучая соотношение между функциями и данными по исходному коду. Имена функций доступа являются глобальными в области действия программы, и возможны конфликты имен. Если разработчики клиентской части применяют в клиенте имена полей данных, то преимущества инкапсуляции исчезают.
C + -f- разрешает эти вопросы за счет ввода в язык конструкций классов. Гра ницы класса показывают, что данные и функции связаны. Каждый класс имеет свою отдельную область действия, и функции доступа с одинаковыми именами в разных классах друг с другом не конфликтуют. Разработчик класса может указать, что данные (и функции) являются закрытыми, и предотвратить доступ к ним из клиентов.
Это впечатляет. Применение классов открывает новые горизонты для разра ботки высококачественных программ. Начиная со следующей главы, мы займемся классами C + + .
^ # / ^ |
^ |
Л!)лассы C++ как единицы модульности программы
Темы данной главы
^Базовый синтаксис классов
^Управление доступом к компонентам классов •^ Инициализация экземпляров объектов
•^ Использование возвращаемых объектов в клиенте
^Подробнее о ключевом слове const •^ Статические компоненты класса
^Итоги
1^^^ предыдущей главе были сформулированы базовые принципы объектно-
^•('^ориентированного программирования с использованием функций как
^^^^^ базовых программных блоков. Применяя при построении программы объектно-ориентированный подход, можно добиться того, что вместо непосред ственного вызова и модификации полей данных клиент будет вызывать функции доступа. Серверные функции выполняют операции для достижения целей клиент ской части. Обязанности распределяются между функциями так, что клиентские функции не знают о представленииданных, а серверные — об алгоритмах клиента.
Врезультате создаются независимые области программы. При изменении функций доступа сопровождающему приложение программисту не нужно вводить изменения в функции-клиенты (если не модифицируется интерфейс сервера).
При изменении клиентских функций программисту нет необходимости разбирать ся в деталях обработки данных в функциях-серверах или в терминах операций с данными — они не нуждаются в изменениях. В клиентском коде используются вызовы серверных функций, а не операции с данными. Совмещение тех элемен тов, которые должны быть вместе (а не разделены), делает функции независимы ми друг от друга и также способствует облегчению сопровождения и повторного использования. Диаграммы объектов, нарисованные в предыдущей главе, пока зывают, что функции-серверы логически связаны друг с другом и с данными, к которым они обращаются.
Отмечалось также, что при применении функций для реализации объектноориентированного подхода приходится полагаться на произвол программиста. Серверные функции могут включаться в не относящиеся друг к другу части ис ходного кода, и разработчик не всегда замечает, что они связаны друг с другом
