Главная страница

Руководство по стилю программирования и конструированию по


Скачать 7.6 Mb.
НазваниеРуководство по стилю программирования и конструированию по
Дата18.05.2023
Размер7.6 Mb.
Формат файлаpdf
Имя файлаCode_Complete.pdf
ТипРуководство
#1139697
страница54 из 104
1   ...   50   51   52   53   54   55   56   57   ...   104
ГЛАВА 19 Общие вопросы управления
435
В языке C сравнивайте символы с нулевым терминатором (‘\0’) явно Сим- волы, как и числа, не являются логическими выражениями. Поэтому для симво- лов следует писать:
while ( *charPtr != ‘\0’ ) ...
а не:
while ( *charPtr ) ...
Эта рекомендация расходится с широко распространенным соглашением языка
C по обработке символьных данных (так, как показано во втором примере). Но первый способ усиливает идею, что выражение имеет дело с символьными, а не с логическими данными. Некоторые соглашения C не основываются на максими- зации удобства чтения или сопровождения, и это — один из таких примеров.
К счастью, весь этот вопрос растворяется в лучах заката, поскольку все больше кода пишется с использованием строк C++ и STL.
Сравнивайте указатели с NULL Для указателей пишите:
while ( bufferPtr != NULL ) ...
а не:
while ( bufferPtr ) ...
Как и в случае с символьными данными, эта рекомендация расходится с устояв- шимся соглашением языка C, но преимущество в читабельности ее оправдывает.
Общие проблемы с логическими выражениями
Логические выражения содержат еще несколько ловушек, специфичных для не- которых языков программирования:
В C-подобных языках, помещайте константы с левой стороны сравнений
В C-подобных языках возникают некоторые специфические проблемы с логиче- скими выражениями. Если время от времени вы набираете
= вместо ==, подумай- те об использовании соглашения по размещению констант и литералов с левой стороны выражений, например, так:
Пример размещения константы с левой стороны выражения на C++.
Это позволит компилятору обнаружить ошибку
if ( MIN_ELEMENTS = i ) ...
В этом выражении компилятор обязан объявить одиночный знак
= ошибкой,
поскольку недопустимо присваивать какое-нибудь значение константе. А в сле- дующем выражении компилятор, напротив, выдаст только предупреждение, и лишь в том случае, если у вас включены все предупреждения компилятора:
Пример размещения константы с правой стороны выражения на C++.
Эту ошибку компилятор может и не обнаружить
if ( i = MIN_ELEMENTS ) ...

436
ЧАСТЬ IV Операторы
Эта рекомендация конфликтует с предложением упорядочивать значения на чис- ловой прямой. Я лично предпочитаю использовать упорядочение, а компилятор пусть предупреждает меня о непреднамеренных присваиваниях.
В C++ рассмотрите вопрос подстановки макросов препроцессора вместо
операторов &&, || и == (но только как последнее средство) Если у вас воз- никают такие трудности, то можно создать макросы
#define для логических опе- раций
and и or и использовать AND и OR вместо && и ||. Точно так же очень легко написать
=, имея в виду ==. Если вы часто с этим сталкиваетесь, можете создать макрос
EQUALS для логического равенства(==).
Многие опытные программисты рассматривают этот подход как упрощающий работу тем разработчикам, которые еще не освоились с языком программирова- ния. А тем, кто уже хорошо знает язык, эта методика будет только мешать. Кроме того, многие компиляторы генерируют предупреждающие сообщения, если исполь- зование присваивания и битовых операций выглядит как ошибка. Включение всех предупреждений компилятора часто является лучшим вариантом, чем создание нестандартных макросов.
В языке Java учитывайте разницу между a==b и a.equals(b) В Java a==b
проверяет, ссылаются ли
a и b на один и тот же объект, тогда как a.equals(b) про- веряет, имеют ли объекты одно и то же логическое значение. В общем случае программы на Java должны использовать выражения вроде
a.equals(b), а не a==b.
19.2. Составные операторы (блоки)
«Составной оператор» или «блок» — это набор операторов, который с точки зре- ния управления ходом программы рассматривается как один оператор. Состав- ные операторы создаются с помощью добавления скобок
{ и } вокруг группы вы- ражений в языках C++, C#, C и Java. Иногда они задаются с помощью ключевых слов команды, как, например,
For и Next в Visual Basic.
Пишите обе скобки одновременно Заполняйте внутрен- нее содержимое по-сле того, как напишете открывающую и закрывающую часть блока. Люди часто жалуются, как тя- жело не ошибиться с парными скобками или словами
begin-
end, хотя это совершенно излишняя проблема. Если вы пос- ледуете этому совету, у вас никогда больше не будет труд- ностей в поиске соответствующих пар скобок.
Сначала напишите:
for ( i = 0; i < maxLines; i++ )
Потом:
for ( i = 0; i < maxLines; i++ ) { }
И наконец:
for ( i = 0; i < maxLines; i++ ) {
// Все что угодно ...
}
Перекрестная ссылка Многие текстовые редакторы, ориенти- рованные на программиста, со- держат команды для поиска пар- ных круглых, квадратных и фи- гурных скобок (см. подраздел
«Редактирование» раздела 30.2).

ГЛАВА 19 Общие вопросы управления
437
Это относится ко всем блоковым структурам, включая операторы
if, for и while в
C++ и Java и сочетания
If-Then-Else, For-Next и While-Wend в Visual Basic.
Используйте скобки для пояснения условных операторов Довольно слож- но разобраться в коде условного оператора, не разбираясь в действиях, выполня- емых в результате проверки условия
if. Размещение единственного оператора после
if иногда выглядит эстетически правильным, но в процессе сопровождения такие выражения часто превращаются в более сложные блоки, и одиночный оператор в таких случаях может привести к ошибке.
Для пояснения ваших намерений используйте блок независимо от того, сколько в нем строк кода: 1 или 20.
19.3. Пустые выражения
В C++ допустимо использования пустого оператора, состоящего исключительно из точки с запятой, например:
Пример традиционного пустого выражения (C++)
while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() )
;
В C++ после
while должно следовать выражение, но оно может быть пустым. От- дельная точка с запятой и является пустым оператором. Далее приведены прин- ципы работы с пустыми выражениями в C++:
Привлекайте внимание к пустым выражениям Пус- тые выражения не являются широко распространенными,
поэтому сделайте их очевидными. Один из способов — вы- делить точке с запятой, представляющей собой пустой опе- ратор, отдельную строку. Этот подход показан в предыду- щем примере. В качестве альтернативы можно использовать пустые скобки, чтобы подчеркнуть это выражение. Приве- дем два примера:
Примеры выделения пустых выражений (C++)
Это один из способов выделить пустое выражение.
while ( recordArray.Read( index++ ) ) != recordArray.EmptyRecord() ) {}
Это еще один способ сделать это.
while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() ) {
;
}
Создайте для пустых выражений макрос препроцессора или встроенную
функцию DoNothing() Это выражение не делает ничего, кроме бесспорного под- тверждения того факта, что никакие действия предприниматься не должны. Это похоже на пометку пустых страниц документа фразами «Эта страница намерен- но оставлена пустой». На самом деле страница не совсем пустая, но вы знаете, что ничего другого на ней быть не должно.
Перекрестная ссылка Возмож- но, лучший способ обрабаты- вать пустые операторы — это избегать их (см. подраздел «Из- бегайте пустых циклов» разде- ла 16.2).
>
>

438
ЧАСТЬ IV Операторы
Вот как создать собственный пустой оператор в C++ с помощью
#define. (Вы так- же можете создать
inline-функцию, которая дает тот же эффект.)
Пример пустого выражения, выделенного с помощью DoNothing() (C++)
#define DoNothing()
while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() ) {
DoNothing();
}
В дополнение к использованию
DoNothing() в пустых циклах while и for можно задействовать ее в несущественных вариантах оператора
switch — добавление
DoNothing() делает очевидным тот факт, что вариант был рассмотрен и никаких действий предприниматься не должно.
Если ваш язык не поддерживает макросы препроцессора или встроенные функ- ции, вы можете создать обычный метод
DoNothing(), который сразу будет возвра- щать управление вызывающей стороне.
Подумайте, не будет ли код яснее с непустым телом цикла Большая часть циклов с пустым телом полагается на побочный эффект в управляющем выраже- нии цикла. В большинстве случаев код будет читабельнее, если эти побочные дей- ствия будут выполняться явно, например:
Пример более очевидного цикла с непустым телом (C++)
RecordType record = recordArray.Read( index );
index++;
while ( record != recordArray.EmptyRecord() ) {
record = recordArray.Read( index );
index++;
}
Этот подход требует дополнительной переменной, управляющей циклом, а так- же большего количества строк, но он делает акцент на простоте программирова- ния, а не на остроумном использовании побочных эффектов. В промышленном коде такой акцент предпочтительней.
19.4. Укрощение опасно глубокой вложенности
Чрезмерные отступы (или «вложенность») осуждаются в компьютерной ли тературе уже на протяжении 25 лет и все еще являются главными обви- няемыми в создании запутанного кода. В работах Ноума Чомски и Дже- ральда Вейнберга (Noam Chomsky and Gerald Weinberg) высказывалось предполо- жение, что немногие люди способны понять более трех уровней вложенных
if
(Yourdon, 1986a), и многие исследователи рекомендуют избегать вложенности, пре- вышающей три или четыре уровня (Myers, 1976; Marca, 1981; Ledgard and Tauer,
1987a). Глубокая вложенность противоречит описанному в главе 5 Главному Тех- ническому Императиву ПО (управлению сложностью). Это достаточная причина для отказа от глубокой вложенности.

ГЛАВА 19 Общие вопросы управления
439
Избавиться от глубокой вложенности несложно. Для этого вы можете переписать проверки условий, выполняемые в блоках
if и else, или раз- бить код на более простые методы.
Упростите вложенные if с помощью повторной проверки части условия
Если вложенность становится слишком глубокой, вы можете уменьшить количе- ство ее уровней, повторно проверив некоторые условия. Глубина вложенности в этом примере кода является достаточным основанием для его реструктуризации:
Пример плохого, глубоко вложенного
кода (C++)
if ( inputStatus == InputStatus_Success ) {
// Много кода.
if ( printerRoutine != NULL ) {
// Много кода.
if ( SetupPage() ) {
// Много кода.
if ( AllocMem( &printData ) ) {
// Много кода.
}
}
}
}
Этот пример придуман для демонстрации уровней вложенности. Части, обозна- ченные как
// Много кода, подразумевают, что метод содержит достаточно много строк и простирается на нескольких экранах или нескольких страницах напеча- танного листинга. Вот как можно видоизменить этот код, используя повторные проверки, а не вложенность:
Пример кода, милосердно избавленного от вложенности
с помощью повторных проверок (C++)
if ( inputStatus == InputStatus_Success ) {
// Много кода.
if ( printerRoutine != NULL ) {
// Много кода.
}
}
if ( ( inputStatus == InputStatus_Success ) &&
( printerRoutine != NULL ) && SetupPage() ) {
// Много кода.
Перекрестная ссылка Повторная проверка части условия для уменьшения сложности анало- гична повторному тестированию статусной переменной. Такой способ демонстрируется в под- разделе «Обработка ошибок и операторы goto» раздела 17.3.

440
ЧАСТЬ IV Операторы if ( AllocMem( &printData ) ) {
// Много кода.
}
}
Это чрезвычайно реалистичный пример, так как показывает, что вы не можете уменьшить уровень вложенности безнаказанно, взамен вам придется формиро- вать более сложный условия. Однако уменьшение с четырех до двух уровней вло- женности дает большое улучшение в читабельности, поэтому такой способ стоит принять во внимание.
Упростите вложенные if с помощью блока с выходом Альтернативой к толь- ко что описанному подходу будет создание фрагмента кода, который будет вы- полняться как блок. Если одно из условий в середине блока не выполнится, оста- ток блока будет пропущен.
Пример использования блока с выходом (C++)
do {
// Начало блока с выходом.
if ( inputStatus != InputStatus_Success ) {
break; // Выходим из блока.
}
// Много кода.
if ( printerRoutine == NULL ) {
break; // Выходим из блока.
}
// Много кода.
if ( !SetupPage() ) {
break; // Выходим из блока.
}
// Много кода.
if ( !AllocMem( &printData ) ) {
break; // Выходим из блока.
}
// Много кода.
} while (FALSE); // Конец блока с выходом
Этот способ довольно необычен, поэтому его следует использовать, только если вся ваша команда разработчиков с ним знакома и он одобрен в качестве подхо- дящей практики кодирования.
Преобразуйте вложенные if в набор if-then-else Если вы критически отно- ситесь к вложенными условиями
if, вам будет интересно узнать, что вы можете ре-

ГЛАВА 19 Общие вопросы управления
441
организовать эти конструкции так, чтобы использовать операторы
if-then-else
вместо вложенных
if. Допустим, у вас есть развесистое дерево решений вроде этого:
Пример заросшего дерева решений (Java)
if ( 10 < quantity ) {
if ( 100 < quantity ) {
if ( 1000 < quantity ) {
discount = 0.10;
}
else {
discount = 0.05;
}
}
else {
discount = 0.025;
}
}
else {
discount = 0.0;
}
Этот фрагмент имеет много недостатков, один из которых в том, что проверяе- мые условия избыточны. Когда вы удостоверились, что значение
quantity больше
1000, вам не нужно дополнительно проверять, что оно больше 100 и больше 10.
А значит, вы можете преобразовать этот код таким образом:
Пример вложенных if, сконвертированных в набор if-then-else (Java)
if ( 1000 < quantity ) {
discount = 0.10;
}
else if ( 100 < quantity ) {
discount = 0.05;
}
else if ( 10 < quantity ) {
discount = 0.025;
}
else {
discount = 0;
}
Это решение проще, чем могло бы быть, потому что закономерность увеличения чисел проста. Вот как изменить вложенные
if, если бы числа не были так упоря- дочены:
Пример вложенных if, преобразованных в набор if-then-else, для случая,
когда числа не упорядочены (Java)
if ( 1000 < quantity ) {
discount = 0.10;
}

442
ЧАСТЬ IV Операторы else if ( ( 100 < quantity ) && ( quantity <= 1000 ) ) {
discount = 0.05;
}
else if ( ( 10 < quantity ) && ( quantity <= 100 ) ) {
discount = 0.025;
}
else if ( quantity <= 10 ) {
discount = 0;
}
Основное различие между этим и предыдущим примером в том, что выражения в условиях
else-if не полагаются на предыдущие проверки. Этот код не требует вы- полнения блока
else, и проверки фактически могут выполняться в любом поряд- ке. Фрагмент мог бы состоять из четырех
if и не включать ни одного else. Един- ственная причина, по которой версия с
else предпочтительней, — это отказ от ненужных повторных проверок.
Преобразуйте вложенные if в оператор case Некоторые виды проверок условий, особенно использующие целые числа, можно перекодировать, применяя оператор
case вместо последовательностей if и else. В некоторых языках вы не сможете использовать эту методику, но там, где это возможно, она очень удобна.
Вот как преобразовать тот же пример на Visual Basic:
Пример преобразования вложенных if к оператору case (Visual Basic)
Select Case quantity
Case 0 To 10
discount = 0.0
Case 11 To 100
discount = 0.025
Case 101 To 1000
discount = 0.05
Case Else discount = 0.10
End Select
Пример читается, как стихи. Если вы сравните его с двумя приведенными ранее
— многочисленными отступами, он покажется особенно понятным решением.
Факторизуйте глубоко вложенный код в отдельный метод Если глубокая вложенность создается внутри цикла, вы зачастую можете улучшить ситуацию, пе- реместив содержимое цикла в отдельный метод. Это особенно эффективно, если вложенность является результатом как проверок условий, так и итераций. Оставьте блоки
if-then-else в основном цикле, чтобы показать ветвление решения, а содер- жимое этих блоков переместите в новые методы. Следующий код нуждается в такой модификации:
Пример вложенного кода, требующего разбиения на методы (C++)
while ( !TransactionsComplete() ) {
// Читаем транзакционную запись.
transaction = ReadTransaction();

ГЛАВА 19 Общие вопросы управления
443
// Обрабатываем транзакцию в зависимости от ее типа.
if ( transaction.Type == TransactionType_Deposit ) {
// Обрабатываем транзакцию-вклад.
if ( transaction.AccountType == AccountType_Checking ) {
if ( transaction.AccountSubType == AccountSubType_Business )
MakeBusinessCheckDep( transaction.AccountNum, transaction.Amount );
else if ( transaction.AccountSubType == AccountSubType_Personal )
MakePersonalCheckDep( transaction.AccountNum, transaction.Amount );
else if ( transaction.AccountSubType == AccountSubType_School )
MakeSchoolCheckDep( transaction.AccountNum, transaction.Amount );
}
else if ( transaction.AccountType == AccountType_Savings )
MakeSavingsDep( transaction.AccountNum, transaction.Amount );
else if ( transaction.AccountType == AccountType_DebitCard )
MakeDebitCardDep( transaction.AccountNum, transaction.Amount );
else if ( transaction.AccountType == AccountType_MoneyMarket )
MakeMoneyMarketDep( transaction.AccountNum, transaction.Amount );
else if ( transaction.AccountType == AccountType_Cd )
MakeCDDep( transaction.AccountNum, transaction.Amount );
}
else if ( transaction.Type == TransactionType_Withdrawal ) {
// Обрабатываем снятие денег.
if ( transaction.AccountType == AccountType_Checking )
MakeCheckingWithdrawal( transaction.AccountNum, transaction.Amount );
else if ( transaction.AccountType == AccountType_Savings )
MakeSavingsWithdrawal( transaction.AccountNum, transaction.Amount );
else if ( transaction.AccountType == AccountType_DebitCard )
MakeDebitCardWithdrawal( transaction.AccountNum, transaction.Amount );
}
Вот транзакция перевода — TransactionType_Transfer.
else if ( transaction.Type == TransactionType_Transfer ) {
MakeFundsTransfer(
transaction.SourceAccountType,
transaction.TargetAccountType,
transaction.AccountNum,
transaction.Amount
);
}
else {
// Обрабатываем неизвестный тип транзакции.
LogTransactionError( “Unknown Transaction Type”, transaction );
}
}
Этот код сложен, но бывает и хуже. Он имеет всего четыре уровня вложенности,
содержит комментарии и логические отступы, а его функциональная декомпози- ция достаточно адекватна, особенно для типа транзакции
TransactionType_Transfer.
И все же вы можете улучшить этот код, вынеся содержимое внутренних
if-прове- рок в отдельные методы.
>

444
ЧАСТЬ IV Операторы
Пример правильно вложенного кода после декомпозиции на методы (C++)
while ( !TransactionsComplete() ) {
// Читаем транзакционную запись.
transaction = ReadTransaction();
// Обрабатываем транзакцию в зависимости от ее типа.
if ( transaction.Type == TransactionType_Deposit ) {
ProcessDeposit(
transaction.AccountType,
transaction.AccountSubType,
transaction.AccountNum,
transaction.Amount
);
}
else if ( transaction.Type == TransactionType_Withdrawal ) {
ProcessWithdrawal(
transaction.AccountType,
transaction.AccountNum,
transaction.Amount
);
}
else if ( transaction.Type == TransactionType_Transfer ) {
MakeFundsTransfer(
transaction.SourceAccountType,
transaction.TargetAccountType,
transaction.AccountNum,
transaction.Amount
);
}
else {
// Обрабатываем неизвестный тип транзакции.
LogTransactionError(“Unknown Transaction Type”, transaction );
}
}
Код новых методов просто был изъят из исходного фраг- мента и оформлен в виде новых методов (они здесь не по- казаны). Такой код имеет несколько преимуществ. Во-пер- вых, двухуровневая вложенность делает структуру проще и понятнее. Во-вторых, вы можете читать, исправлять и отла- живать более короткий цикл
while, помещающийся на од- ном экране — не требуется переходить между экранами или страницами напечатанного текста. В-третьих, при вынесе- нии функциональности в методы
ProcessDeposit() и ProcessWithdrawal() приобре- таются все остальные преимущества модульности. В-четвертых, теперь легко можно увидеть, что этот код может быть преобразован в оператор
case, что еще более упростит чтение:
Перекрестная ссылка Этот спо- соб функциональной декомпози- ции особенно прост, если вы из- начально строили методы по ме- тодике, описанной в главе 9.
О принципах функциональной де- композиции см. подраздел «Раз- деляй и властвуй» раздела 5.4.

1   ...   50   51   52   53   54   55   56   57   ...   104


написать администратору сайта