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

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

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

330 Часть 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 • Программирование с использованием функций

331

bool isLeft (int i)

// получить символ избуфера

{ char с = buffer[i];

return (c=='(' IIc=='['); }

// проверить, левая ли это скобка

bool isRight (int i)

// получить символ избуфера

 

{ char с = buffer[i];

 

return (c==')' I Ic==' ]' }

// проверить, правая ли это скобка

bool symbolsMatch (int idx, int i)

// получить два символа

 

{ char sym = store[idx], с = buffer[i];

 

 

// для сравнения

 

return (sym=='('&&с==')')||(sym=='['&&c==']'); }

// совпадают ли они?

В листинге 8.11 показана версия программы, использующая данные функции доступа. Если приложение должно работать с фигурными скобками, то потребу­

ется изменить только функции isLeftO, isRightO и symbolsMatch(). Функции checkParen()или другой клиентский код модифицировать не потребуется. Резуль­ тат данной версии программы будет тем же, что и программы из листинга 8.11.

Листинг 8.11. Пример инкапсуляции собщей

информацией

#include <iostream>

// плохой обмен информацией

#inclucle <cstring>

 

using namespace std;"

 

char buffer[81]; char store[81];

bool isLeft (int i) { char с = buffer[i];

return (c=='(' IIc=='['); }

bool isRight (int i) { char с = buffer[i];

return (c==')' 11c==']'); }

bool symbolsMatch (int idx, int i)

{ char sym = store[idx], с = buffer[i]; return (sym==' ('&&c==')')||(sym=='['&&c^

bool checkParen ()

{char c; int i, idx; bool valid;

i= 0; idx = 0; valid =true;

while (buffer[i] != '\0' &&valid) { с = buffer[i];

if (isLeft(i))

{store[idx] = c; idx++; } else if (isRight(i))

if (idx > 0)

{idx-;

if (! (symbolsMatch[idx,i]) valid = false; }

else

valid = false; i++; }

if (idx > 0) valid = false; return valid; }

void checkParenTest(char expression[])

{strcpy(buffer,expression);

cout « "Выражение " « buffer « endl;

//глобальные данные

//получить символ избуфера

//проверить, что это левая скобка

//получить символ избуфера

//проверить, что это правая скобка

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

]'); } // совпадают ли они?

//инициализировать данные

//конец данных или ошибка?

//получить следующий символ

//следующая скобка - открывающая?

//затем сохранить ее

//следующая - закрывающая?

//существует ли сохраненный символ?

//получить последний символ

//если непарные

//тогда ошибка

//если нет парного сохраненного символа, ошибка

//перейти к следующему символу

//непарная левая скобка - ошибка

//возврат статуса ошибки

//тестирующая функция

//вывод выражения

332

 

Часть II ^ Объектно-ориентированное oporpaiwir^HpOBaHHe на С^-Ф

i f

(checkParenO)

/ /

проверить допустимость

 

cout

«

"допустимо\п";

/ /

вывод результата

else

 

 

 

 

 

cout

«

"недопустимо\п";}

 

 

int

mainO

 

 

/ /

инициализатор тестов

{

 

 

 

/ /

первый тест: допустимое выражение

checkParenTest("a=(x[i]+5)*y;");

checkParenTest("a=(x[i]+5]*y;");

/ /

второй тест: недопустимое выражение

return 0;

 

 

 

 

 

 

Хорошая ли это инкапсуляция? Не очень. Представление символов (скобок)

 

 

 

действительно скрыто

от клиента, но серверные функции работают не только

 

 

 

с представлением символов и правилами сравнения. Совместно с клиентом они

 

 

 

оперируют массивами

buffer[] и store[]. Обязанности клиента и функций-

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

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

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

Для устранения подобного недостатка попробуем снова перепроектировать программу и изменить распределение обязанностей. Передавая функциям сами символы, а не их индексы, скроем индексы массивов от функций-серверов. В вер­ сии, показанной в листинге 8.12, функции доступа к символам isLeftO, isRightO и symbolsMatchO знают только о символах, а не о способе их хранения. О масси­ вах знает только клиент.

Листинг 8.12. Улучшенный пример инкапсуляции

#inclucle <iostream> «include <cstring> using namespace std;

bool isLeft (char

c)

{

return ( c - ' C

II c - ='[*); }

bool isRight (char

c)

{

return (c==')'

II

c = = ' ] ' ) ; }

bool symbolsMatch

(char c, char sym)

{

return (sym=='('&&c==')')||(sym=='['&&c==']'); }

bool checkParen (char buffer[])

{

char store[81];

 

 

char csym; int i, idx; bool valid;

 

i = 0; idx = 0; valid =true;

//лучший обмен информацией

//проверить, что это левая скобка

//проверить, что это правая скобка

//совпадают ли они?

//выражение - параметр

//локальный массив

//инициализировать данные

 

 

 

Глава 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[].

334

Часть II # Объектно-ориентированное орогрог^^ирование но С^-Ф

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

struct Store {

 

 

 

/ /

массив для временного хранения

char а[81];

 

 

 

int

idx;

};

 

 

 

/ /

индекс

первой доступной ячейки

void

initStore

(Store

&s)

/ /

индекс

пустой ячейки

{ s.idx

= 0;

}

 

 

bool

isEmpty (const

Store&

s)

 

 

 

{ return

(s.idx == 0); }

/ /

проверка,

пуст ли store

void

saveSymbol (Store

&s,

char x)

 

 

 

{ s.a[s.idx] = x;

 

 

/ /

сохранить

символ в store

s.idx++;

}

 

 

 

 

 

 

 

char

getLast(Store &s)

 

/ /

вернуться к последнему сохраненному символу

{ s.idx-;

 

 

 

 

return s . a[s . idx];

}

 

 

 

 

 

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

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

Листинг 8.13. Инкапсуляция временного хранения store[]

#include <iostream> #inclucle <cstring> using namespace std;

struct Store { char a[81]; int idx; };

void

initStore

(Store

&s)

 

{

s.idx = 0;

}

 

 

 

bool

isEmpty

(const

Store&

s)

{

return (s.idx == 0); }

 

void

saveSymbol

(Store

&s,

char x)

{ s.a[s.idx++] = x;

 

 

 

char

getLast(Store

&s)

 

 

{ return s . a[ - s . idx];

}

 

bool

isLeft

(char c)

 

 

{ return (c=='('

M

c = = ' [ ' ) ; }

bool

isRight

(char

c)

 

 

{ return (c==')'

11 c=

] ' ; )

/ / инкапсуляция с сокрытием информации

//массив для временного хранения

//индекс первой доступной ячейки

//индекс пустой ячейки

//проверка, пуст ли store

//сохранить символ в 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^^^ предыдущей главе были сформулированы базовые принципы объектно-

^•('^ориентированного программирования с использованием функций как

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

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

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

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

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