Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 14 Организация последовательного кода 343 первым и последним своим упоминанием. Иначе говоря, вам придется просмот# реть и осмыслить каждую строку кода в этом фрагменте, чтобы выяснить, как вычисляется marketingData. И, разумеется, этот пример гораздо проще, чем код, встречающийся в реальных системах. Вот тот же код, но лучше организованный: Пример хорошего, последовательного кода, который читается сверху вниз (C++) MarketingData marketingData; marketingData.ComputeQuarterly(); marketingData.ComputeAnnual(); marketingData.Print(); SalesData salesData; salesData.ComputeQuarterly(); salesData.ComputeAnnual(); salesData.Print(); TravelData travelData; travelData.ComputeQuarterly(); travelData.ComputeAnnual(); travelData.Print(); Этот код лучше по нескольким причинам. Упоминания каж# дой переменной располагаются вместе — они «локализова# ны». Число строк кода, в которых объекты являются «живы# ми», невелико. И, возможно, самое важное: код теперь вы# глядит так, что его можно разбить на отдельные методы для данных по маркетингу, продажам и поездкам. Первый фраг# мент не содержал подсказки, что эта декомпозиция возможна. Группировка взаимосвязанных выражений Размещайте взаимосвязанные выражения вместе. Они мо# гут быть связаны, так как работают с одними и теми же дан# ными, выполняют схожие задачи или зависят от порядка выполнения друг друга. Есть простой способ убедиться, что взаимосвязанные выра# жения хорошо сгруппированы. Распечатайте текст вашего метода и обведите рамкой взаимосвязанные выражения. Если они хорошо упоря# дочены, вы получите картинку, похожую на рис. 14#1, где рамки не перекрываются. Перекрестная ссылка Более формальное определение «жи- вых» переменных см. в подраз- деле «Измерение времени жиз- ни переменной» раздела 10.4. Перекрестная ссылка Если вы придерживаетесь Процесса Про- граммирования с Псевдокодом, ваш код будет автоматически груп- пироваться в блоки взаимосвязан- ных выражений (см. главу 9). 344 ЧАСТЬ IV Операторы Рис. 14'1. Если код хорошо организован в группы, то рамки вокруг взаимосвязанных разделов не перекрываются; они могут быть вложенными Если выражения плохо организованы, вы получите картинку, похожую на рис. 14#2, где рамки перекрываются. Если выяснится, что перекрытие происходит, реорганизуйте ваш код, чтобы взаимосвязанные выражения были лучше сгруппированы. Рис. 14'2. Если код организован неудачно, то рамки вокруг связанных разделов пересекаются После того, как вы сгруппируете взаимосвязанные выражения, может выяснить# ся, что они сильно связаны между собой, а к предшествующему и последующему коду не имеют значимого отношения. В этом случае, возможно, следует выделить эти выражения в отдельный метод. Контрольный список: организация последовательного кода Способствует ли код выявлению зависимостей между выражениями? Способствуют ли имена методов выявлению зависимостей? Способствуют ли параметры методов выявлению зависимостей? Описывают ли комментарии такие зависимости, которые иначе не будут явными? Используются ли вспомогательные переменные для проверки последователь- ных действий в критических частях кода? Возможно ли прочтение кода сверху вниз? Сгруппированы ли вместе взаимосвязанные выражения? Перенесены ли относительно независимые группы выражений в отдельные методы? Перекрестная ссылка Об объе- динении операций над перемен- ными см. раздел 10.4. http://cc2e.com/1472 ГЛАВА 14 Организация последовательного кода 345 Ключевые моменты Главный принцип организации последовательного кода — упорядочение за# висимостей. Зависимости должны быть сделаны явными с помощью хороших имен мето# дов, списков параметров, комментариев и — если последовательность кода достаточно критична — с помощью вспомогательных переменных. Если порядковые зависимости в коде отсутствуют, старайтесь размещать вза# имосвязанные выражения как можно ближе друг к другу. 346 ЧАСТЬ IV Операторы Г Л А В А 1 5 Условные операторы Содержание 15.1. Операторы if 15.2. Операторы case Связанные темы Укрощение глубокой вложенности: раздел 19.4 Общие вопросы управления: глава 19 Код с операторами цикла: глава 16 Последовательный код: глава 14 Отношения между типами данных и управляющими структурами: раздел 10.7 Условный оператор управляет выполнением других операторов. Их выполнение «обусловливают» такие операторы, как if, else, case и switch. Хотя операторы цик# ла, например while и for, по смыслу тоже можно отнести к условным, обычно их рассматривают отдельно. Операторы while и for мы обсудим в главе 16. 15.1. Операторы if В зависимости от выбранного языка программирования вы можете использовать несколько видов if#операторов. Простейшие из них — if или if%then. Оператор if% then%else немного сложнее, а наибольшую сложность представляют последователь# ности if%then%else%if. Простые операторы if-then Следуйте этим правилам при написании if#выражений. Сначала напишите код номинального хода алгоритма, затем опишите исключительные случаи Пишите код так, чтобы нормаль# ный путь выполнения был очевиден. Убедитесь, что нестандартные об# стоятельства не затмевают смысл основного алгоритма. Это важно как с точки зрения читабельности, так и с точки зрения производительности. Убедитесь, что при сравнении на равенство ветвление корректно Исполь# зование > вместо >= или < вместо <= — это аналог ошибки потери единицы при http://cc2e.com/1538 ГЛАВА 15 Условные операторы 347 обращении к массиву или вычислении индекса цикла. Чтобы ее избежать, в опе# раторе цикла следует рассматривать граничные точки, а в условных операторах — учитывать случаи равенства. Размещайте нормальный вариант после if, а не пос' ле else Пишите код так, чтобы нормальный вариант разви# тия событий обрабатывался в первую очередь. Это совпада# ет с главным принципом размещения действий, являющих# ся результатом выбора, как можно ближе к точке этого вы# бора. Вот пример кода, который выполняет неоднократную проверку ошибок, бес# порядочно разбросанную по тексту: Пример кода, который беспорядочно обрабатывает многочисленные ошибки (Visual Basic) OpenFile( inputFile, status ) If ( status = Status_Error ) Then Ошибочная ситуация. errorType = FileOpenError Else Нормальная ситуация. ReadFile( inputFile, fileData, status ) If ( status = Status_Success ) Then Нормальная ситуация. SummarizeFileData( fileData, summaryData, status ) If ( status = Status_Error ) Then Ошибочная ситуация. errorType = ErrorType_DataSummaryError Else Нормальная ситуация. PrintSummary( summaryData ) SaveSummaryData( summaryData, status ) If ( status = Status_Error ) Then Ошибочная ситуация. errorType = ErrorType_SummarySaveError Else Нормальная ситуация. UpdateAllAccounts() EraseUndoFile() errorType = ErrorType_None End If End If Else Перекрестная ссылка Другие способы обращения с кодом обработки ошибок описаны в разделе 19.4. > > > > > > > 348 ЧАСТЬ IV Операторы errorType = ErrorType_FileReadError End If End If Этот код сложен для понимания, так как в нем перемешаны нормальные и оши# бочные ситуации. Тяжело проследить путь, проходимый в коде при нормальных обстоятельствах. Кроме того, так как ошибки иногда обрабатываются в блоке if, а не else, тяжело найти, в каких же ветвях if обрабатываются нормальные ситуации. В переписанном примере нормальный путь последовательно кодируется первым, а ошибочные ситуации — последними. Это упрощает поиск и чтение номиналь# ного варианта алгоритма. Пример кода, который систематично обрабатывает большое количество ошибок (Visual Basic) OpenFile( inputFile, status ) If ( status = Status_Success ) Then Нормальная ситуация. ReadFile( inputFile, fileData, status ) If ( status = Status_Success ) Then Нормальная ситуация. SummarizeFileData( fileData, summaryData, status ) If ( status = Status_Success ) Then Нормальная ситуация. PrintSummary( summaryData ) SaveSummaryData( summaryData, status ) If ( status = Status_Success ) Then Нормальная ситуация. UpdateAllAccounts() EraseUndoFile() errorType = ErrorType_None Else Ошибочная ситуация. errorType = ErrorType_SummarySaveError End If Else Ошибочная ситуация. errorType = ErrorType_DataSummaryError End If Else Ошибочная ситуация. errorType = ErrorType_FileReadError End If Else > > > > > > > ГЛАВА 15 Условные операторы 349 Ошибочная ситуация. errorType = ErrorType_FileOpenError End If Здесь можно проследить главное направление if#проверок, чтобы выяснить нор# мальный вариант событий. Этот фрагмент позволяет фокусировать чтение в ос# новном направлении, а не преодолевать исключительные ситуации, поэтому этот код в целом читабельнее. Стек ошибочных условий, расположенный внизу вло# жения, — признак хорошо написанного кода обработки ошибок. Этот пример иллюстрирует один систематический подход к обработке нормаль# ных и ошибочных ситуаций. Другие решения этой проблемы: использование пре# дохранительных конструкций, диспетчеризация полиморфных объектов, выне# сение внутренних проверок в отдельные методы — обсуждаются на протяжении всей книги. Полный список существующих подходов см. в разделе 19.4. Размещайте осмысленные выражения после оператора if Иногда можно встретить код, в котором блок if пуст: Пример пустого блока if (Java) if ( SomeTest ) ; else { // делаем чтото } Опытные программисты не станут писать такой код хотя бы затем, чтобы избежать лишней работы по вводу пустой стро# ки и оператора else. Этот код выглядит глупо и может быть легко улучшен путем отрицания предиката в выражении if, перемещения кода из блока else в блок if и удаления блока else. Вот как будет выглядеть код после таких изменений: Пример преобразования пустого блока if (Java) if ( ! someTest ) { // делаем чтото } Рассмотрите вопрос использования блока else Если вы считаете, что вам нужен простой оператор if, подумайте, может, вам на самом деле нужен вариант с if%then%else. Классический анализ General Motors пока# зал, что в 50–80% случаев использования операторов if следовало применять и оператор else (Elshoff, 1976). Одна из причин добавления блока else — даже пустого — в том, чтобы продемон# стрировать, что вариант с else был учтен. Конечно, кодирование пустых выраже# ний в else просто для того, чтобы показать, что этот вариант рассмотрен, может Перекрестная ссылка Один из ключей к конструированию эф- фективного оператора if- — со- здание правильного управляюще- го логического выражения. Об эф- фективном применении логичес- ких выражений см. раздел 19.1. > 350 ЧАСТЬ IV Операторы быть преувеличением, но хотя бы принимайте вариант с else во внимание. Если вы задаете if#проверку, не имеющую блока else, то, кроме очевидных случаев, пи# шите в комментариях объяснение, почему else отсутствует, скажем, так: Пример полезного, прокомментированного блока elsе (Java) // Если цвет задан корректно. if ( COLOR_MIN <= color && color <= COLOR_MAX ) { // Делаем чтото } else { // Иначе цвет задан некорректно. // Вывод на экран не выполняется – просто игнорируем команду. } Проверяйте корректность выражения else При тестировании кода вы мо# жете решить, что достаточно проверить основной блок if и все. Однако, если можно проверить вариант в else, не забудьте это сделать. Проверяйте возможную перестановку блоков if и else Частая ошибка при программировании выражений if%then состоит в размещении кода из блока if в блоке else, т. е. инвертировании логики выражения if. Проверяйте ваш код на наличие этой ошибки. Последовательности операторов if-then-else Если язык не поддерживает операторы case или поддерживает их только частич# но, вам часто придется писать последовательные проверки if%then%else. К приме# ру, код распределения символов по категориям, может выглядеть в виде такой цепочки: Пример использования последовательности if-then-else для распределения символов по категориям (C++) if ( inputCharacter < SPACE ) { characterType = CharacterType_ControlCharacter; } else if ( inputCharacter == ‘ ‘ || inputCharacter == ‘,’ || inputCharacter == ‘.’ || inputCharacter == ‘!’ || inputCharacter == ‘(‘ || inputCharacter == ‘)’ || inputCharacter == ‘:’ || inputCharacter == ‘;’ || inputCharacter == ‘?’ || inputCharacter == ‘’ ) { characterType = CharacterType_Punctuation; } Перекрестная ссылка Об упро- щении сложных выражений см. раздел 19.1. ГЛАВА 15 Условные операторы 351 else if ( ‘0’ <= inputCharacter && inputCharacter <= ‘9’ ) { characterType = CharacterType_Digit; } else if ( ( ‘a’ <= inputCharacter && inputCharacter <= ‘z’ ) || ( ‘A’ <= inputCharacter && inputCharacter <= ‘Z’ ) ) { characterType = CharacterType_Letter; } Учитывайте советы, приведенные далее, при написании последовательных if%then%else. Упрощайте сложные проверки с помощью вызовов логических функций Одна из причин, по которой код из предыдущего примера сложно читать, в том, что проверки категорий символов довольно сложны. Для улучшения читабельно# сти вы можете заменить их вызовами функций, возвращающих логические зна# чения. Вот как этот пример может выглядеть после замены условий логическими функциями: Пример последовательности if-then-else, использующей вызовы логических функций (C++) if ( IsControl( inputCharacter ) ) { characterType = CharacterType_ControlCharacter; } else if ( IsPunctuation( inputCharacter ) ) { characterType = CharacterType_Punctuation; } else if ( IsDigit( inputCharacter ) ) { characterType = CharacterType_Digit; } else if ( IsLetter( inputCharacter ) ) { characterType = CharacterType_Letter; } Размещайте наиболее вероятные варианты раньше остальных Помес# тив в начало наиболее часто встречающиеся ситуации, вы минимизируете то ко# личество кода, обрабатывающего исключительные случаи, которое придется про# читать при поиске обычных вариантов. Вы увеличите эффективность, потому что уменьшите число проверок, выполняемых кодом в большинстве случаев. В при# веденном примере буквы обычно встречаются чаще, чем знаки пунктуации, но проверка этих знаков написана первой. Вот как исправить код, чтобы буквы про# верялись в первую очередь: Пример проверки прежде всего наиболее часто встречающихся вариантов (C++) Этот случай встречается чаще других, поэтому проверяем его первым. if ( IsLetter( inputCharacter ) ) { characterType = CharacterType_Letter; } > 352 ЧАСТЬ IV Операторы else if ( IsPunctuation( inputCharacter ) ) { characterType = CharacterType_Punctuation; } else if ( IsDigit( inputCharacter ) ) { characterType = CharacterType_Digit; } Этот случай наиболее редкий, поэтому проверяем его последним. else if ( IsControl( inputCharacter ) ) { characterType = CharacterType_ControlCharacter; } Убедитесь, что учтены все варианты Закодируйте в последнем блоке else сообщение об ошибке или утвержде# ние, чтобы отловить ситуации, которые вы не планирова# ли. Это сообщение об ошибке предназначено скорее вам, а не пользователю, так что сформулируйте его соответственно. Вот как модифицировать пример классификации символов для выполнения проверки «других вариантов»: Пример использования варианта по умолчанию для перехвата ошибок (C++) if ( IsLetter( inputCharacter ) ) { characterType = CharacterType_Letter; } else if ( IsPunctuation( inputCharacter ) ) { characterType = CharacterType_Punctuation; } else if ( IsDigit( inputCharacter ) ) { characterType = CharacterType_Digit; } else if ( IsControl( inputCharacter ) ) { characterType = CharacterType_ControlCharacter; } else { DisplayInternalError( “Unexpected type of character detected.” ); } Замените последовательности ifthenelse другими конструкциями, кото' рые поддерживает ваш язык программирования Некоторые языки, скажем, Microsoft Visual Basic и Ada, предоставляют операторы case, поддерживающие стро# ки, перечисления и логические функции. Используйте их — они проще в написа# нии и чтении, чем цепочки if%then%else. Код классификации типов символов, на# писанный на Visual Basic с помощью оператора case, может выглядеть так: Пример использования оператора case вместо последовательности if-then-else (Visual Basic) Select Case inputCharacter Case “a” To “z” characterType = CharacterType_Letter Перекрестная ссылка Это так- же хороший пример того, как использовать последователь- ность if-then-else вместо кода глубокой вложенности. Об этой методике см. раздел 19.4. > |