Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 7 Высококачественные методы 177 Постарайтесь всегда проверять типы аргументов в списках параметров и внима# тельно изучайте предупреждения компилятора о несоответствии типов параметров. 7.6. Отдельные соображения по использованию функций Современные языки, такие как C++, Java и Visual Basic, поддерживают и функции, и процедуры. Функция — это метод, возвращающий значение; процедуры значе# ний не возвращают. В C++ все методы обычно называют «функциями», однако, с точки зрения семантики, функция, «возвращающая» void, является процедурой. Различие между функциями и процедурами выражено в семантике не слабее, чем в синтаксисе, и именно семантике следует уделять наибольшее внимание. Когда использовать функцию, а когда процедуру? Пуристы утверждают, что функция должна возвращать только одно значение по# добно математической функции. В этом случае все функции принимали бы толь# ко входные параметры и возвращали единственное значение традиционным пу# тем. Функции всегда назывались бы в соответствии с возвращаемым значением: sin(), CustomerID(), ScreenHeight() и т. д. Процедуры, с другой стороны, могли бы принимать входные, изменяемые и выходные параметры в любом количестве. Довольно часто можно встретить функцию, работающую как процедура, но воз# вращающую при этом код статуса. Логически она является процедурой, но из#за возврата значения официально ее следует называть функцией. Так, объект report мог бы иметь метод FormatOutput(), используемый подобным образом: if ( report.FormatOutput( formattedReport ) = Success ) then ... В этом примере метод report.FormatOutput() работает как процедура в том смыс# ле, что он имеет входной параметр formattedReport, но технически это функция, потому что сам метод тоже возвращает значение. В защиту этого подхода вы мог# ли бы сказать, что возвращаемое функцией значение не имеет отношения ни к главной цели функции (форматированию вывода), ни к имени метода ( report.% FormatOutput()). В этом смысле метод больше похож на процедуру, пусть даже технически он является функцией. Если возвращаемое значение служит для определения успеха или неудачи выполнения процедуры согласованно, это не вызывает замешательства. Альтернативный подход — создание процедуры, принимающей переменную ста# туса в качестве явного параметра, например: report.FormatOutput( formattedReport, outputStatus ) if ( outputStatus = Success ) then ... Я предпочитаю именно этот вариант, но не потому, что трепетно отношусь к раз# личию между функциями и процедурами, а потому, что такой код ясно разделяет вызов метода и проверку переменной статуса. Объединение вызова и проверки в одной строке увеличивает «плотность» команды, а значит, и ее сложность. Следу# ющий вариант использования функции также хорош: 178 ЧАСТЬ II Высококачественный код outputStatus = report.FormatOutput( formattedReport ) if ( outputStatus = Success ) then ... Словом, используйте функцию, если основная цель метода — возврат значения, указанного в имени функции. Иначе применяйте процедуру. Возврат значения из функции Использование функции сопряжено с риском того, что функция возвратит некор# ректное значение. Обычно это объясняется наличием нескольких путей выпол# нения функции, один из которых не устанавливает возвращаемого значения. Следуя моим советам, вы сведете этот риск к минимуму. Проверяйте все возможные пути возврата Создав функцию, проработай# те в уме каждый возможный путь ее выполнения, дабы убедиться, что функция воз# вращает значение во всех возможных обстоятельствах. Целесообразно инициа# лизировать возвращаемое значение в начале функции значением по умолчанию: это будет страховкой на тот случай, если функция не установит корректное воз# вращаемое значение. Не возвращайте ссылки или указатели на локальные данные Как только выполнение метода завершается и локальные данные выходят из области види# мости, ссылки и указатели на локальные данные становятся некорректными. Если объект должен возвращать информацию о своих внутренних данных, пусть он сохранит ее в форме данных — членов класса. Реализуйте для него функции до# ступа, возвращающие данные#члены, а не ссылки или указатели на локальные данные. 7.7. Методы-макросы и встраиваемые методы С методами#макросами связаны некоторые уникальные со# ображения. Следующие правила и примеры рассматриваются в контексте препроцессора C++. Если вы используете дру# гой язык или препроцессор, адаптируйте правила к своей ситуации. Разрабатывая макрос, заключайте в скобки все, что можно Так как макросы и их аргументы расширяются в код, следите за тем, чтобы они расширялись так, как вам нужно. Одну частую проблему иллюстрирует сле# дующий макрос: Пример макроса, который расширяется неверно (C++) #define Cube( a ) a*a*a Если вы передадите в этот макрос неатомарное значение a, он выполнит умноже# ние неверно. Так, выражение Cube( x+1 ) расширится в x+1 * x + 1 * x + 1, что из#за приоритета операций умножения и сложения приведет к получению ошибочного результата. Вот улучшенная, но все еще не совсем правильная версия этого макроса: Перекрестная ссылка Даже если ваш язык не поддерживает пре- процессор макросов, вы може- те создать собственный препро- цессор (см. раздел 30.5). ГЛАВА 7 Высококачественные методы 179 Пример макроса, который все еще расширяется неверно (C++) #define Cube( a ) (a)*(a)*(a) Цель уже близка. Однако, если вы используете макрос Cube() в выражении, включа# ющем операции с более высоким приоритетом, чем умножение, выражение (a)*(a)*(a) будет вычислено неверно. Что делать? Заключите в скобки все выражение: Пример макроса, с которым все в порядке (C++) #define Cube( a ) ((a)*(a)*(a)) Заключайте макрос, включающий несколько команд, в фигурные скобки Макрос может включать несколько команд, что может привести к проблемам, если вы будете рассматривать их как единый блок, например: Пример неправильного макроса, состоящего из нескольких команд (C++) #define LookupEntry( key, index ) \ index = (key 10) / 5; \ index = min( index, MAX_INDEX ); \ index = max( index, MIN_INDEX ); for ( entryCount = 0; entryCount < numEntries; entryCount++ ) LookupEntry( entryCount, tableIndex[ entryCount ] ); Этот макрос работает не так, как работал бы обычный метод: единственной час# тью макроса, выполняемой в цикле for, является первая строка: index = (key - 10) / 5; Чтобы устранить эту проблему, заключите макрос в фигурные скобки: Пример правильного макроса, состоящего из нескольких команд (C++) #define LookupEntry( key, index ) { \ index = (key 10) / 5; \ index = min( index, MAX_INDEX ); \ index = max( index, MIN_INDEX ); \ } Замена вызовов методов макросами обычно считается рискованным и малопонят# ным (короче, плохим) подходом, так что используйте его только при необходи# мости. Называйте макросы, расширяющиеся в код подобно методам, так, что' бы при необходимости их можно было заменить методами Конвенция именования макросов в C++ подразумевает использование только заглавных букв. Если же макрос может быть заменен методом, называйте его в соответствии с конвенцией именования методов. Это позволит вам заменять макросы на методы и наоборот, не изменяя остального кода. 180 ЧАСТЬ II Высококачественный код Следование этой рекомендации связано с риском. Если вы часто используете операции ++ и –– ради их побочных эффектов (в составе других выражений), то, принимая макросы за методы, вы столкнетесь с неприятностями. Это еще одна причина избегать побочных эффектов. Ограничения использования методов-макросов Современные языки вроде C++ поддерживают много альтернатив макросам: ключевое слово const для объявления констант; ключевое слово inline для определения функций, которые будут компилиро# ваться как встраиваемый код; шаблоны для безопасного в плане типов определения стандартных операций, таких как min, max и т. д.; ключевое слово enum для определения перечислений; директиву typedef для простых замен одного типа другим. Бьерн Страуструп, создатель C++, пишет: «Макрос почти всегда указыва# ет на недостаток языка программирования, программы или программи# ста… Если вы используете макросы, значит, вам не хватает возможностей отладчиков, инструментов, генерирующих перекрестные ссылки, средств профи# лирования и т. д.» (Stroustrup, 1997). Макросы полезны для выполнения условной компиляции (см. раздел 8.6), но добросовестные программисты обычно исполь# зуют макросы вместо методов только в крайнем случае. Встраиваемые методы Язык C++ поддерживает ключевое слово inline, служащее для определения встра# иваемых методов. Иначе говоря, программист может разрабатывать код как ме# тод, но во время компиляции компилятор постарается встроить каждый экземп# ляр метода прямо в код. Теоретически встраивание методов может повысить бы# стродействие кода, позволяя избежать затрат, связанных с вызовами методов. Не злоупотребляйте встраиваемыми методами Встраиваемые методы на# рушают инкапсуляцию, потому что C++ требует, чтобы программист поместил код встраиваемого метода в заголовочный файл, доступный остальным программистам. При встраивании метода каждый его вызов заменяется на полный код метода, что во всех случаях увеличивает объем кода и само по себе может создать проблемы. Практическое применение встраивания аналогично применению прочих мето# дик повышения быстродействия кода: профилируйте код и оценивайте результа# ты. Если ожидаемое повышение быстродействия не оправдывает забот, связанных с профилированием, нужным для проверки выгоды, оно не оправдывает и сни# жения качества кода. Контрольный список: высококачественные методы Общие вопросы Достаточна ли причина создания метода? Все ли части метода, которые целесообразно поместить в отдельные методы, сделаны отдельными методами? http://cc2e.com/0792 ГЛАВА 7 Высококачественные методы 181 Имеет ли имя процедуры вид «выразительный глагол + объект»? Описывает ли имя функции возвращаемое из нее значение? Описывает ли имя метода все выполняемые в методе действия? Задали ли вы конвенции именования часто выполняе- мых операций? Имеет ли метод высокую функциональную связность? Решает ли он только одну задачу и хорошо ли он с ней справляется? Имеют ли методы слабое сопряжение? Являются ли связи метода с други- ми методами малочисленными, детальными, заметными и гибкими? Обусловлена ли длина метода его ролью и логикой, а не искусственным стандартом кодирования? Передача параметров Формирует ли в целом список параметров метода согласованную абстрак- цию интерфейса? Разумно ли упорядочены параметры метода? Соответствует ли их порядок порядку параметров аналогичных методов? Документированы ли выраженные в интерфейсе предположения? Метод имеет семь параметров или меньше? Все ли входные параметры используются? Все ли выходные параметры используются? Не используются ли входные параметры в качестве рабочих переменных? Если метод является функцией, возвращает ли он корректное значение во всех возможных случаях? Ключевые моменты Самая важная, но далеко не единственная причина создания методов — улуч# шение интеллектуальной управляемости программы. Сокращение кода — не такая уж и важная причина; повышение его удобочитаемости, надежности и облегчение его изменения куда важнее. Иногда огромную выгоду можно извлечь, создав отдельный метод для простой операции. Связность методов можно разделить на несколько видов. Самая лучшая — функ# циональная — достижима практически всегда. Имя метода является признаком его качества. Плохое, но точное имя часто указывает на плохое проектирование метода. Плохое и неточное имя не опи# сывает роль метода. Как бы то ни было, плохое имя предполагает, что программу нужно изменить. Функцию следует использовать, только когда главной целью метода является возврат конкретного значения, описываемого именем функции. Добросовестные программисты используют методы#макросы с осторожностью и только в крайнем случае. Перекрестная ссылка Этот кон- трольный список позволяет определить качество методов. Вопросы, касающиеся этапов создания метода, приведены в контрольном списке «Процесс Программирования Псевдокода» (глава 9). 182 ЧАСТЬ II Высококачественный код Г Л А В А 8 Защитное программирование Содержание 8.1. Защита программы от неправильных входных данных 8.2. Утверждения 8.3. Способы обработки ошибок 8.4. Исключения 8.5. Изоляция повреждений, вызванных ошибками 8.6. Отладочные средства 8.7. Доля защитного кода в промышленной версии 8.8. Защита от защитного программирования Связанные темы Сокрытие информации: подраздел «Скрывайте секреты (к вопросу о сокрытии информации)» раздела 5.3 Дизайн изменений: подраздел «Определите области вероятных изменений» раз# дела 5.3 Архитектура программного обеспечения: раздел 3.5 Дизайн в проектировании: глава 5 Отладка: глава 23 Защитное программирование не означает защиту своего кода словами: «Это так работает!» Его идея совпадает с идеей внимательного вождения, при котором вы готовы к любым выходкам других водителей: вы не по# страдаете, даже если они совершат что#то опасное. Вы берете на себя ответствен# ность за собственную защиту и в тех случаях, когда виноват другой водитель. В защитном программировании главная идея в том, что если методу передаются некорректные данные, то его работа не нарушится, даже если эти данные испор# чены по вине другой программы. Обобщая, можно сказать, что в программах всегда будут проблемы, программы будут модифицироваться и разумный программист будет учитывать это при разработке кода. http://cc2e.com/0861 ГЛАВА 8 Защитное программирование 183 Эта глава рассказывает, как защититься от беспощадного мира неверных данных, событий, которые «никогда» не могут случиться, и других программистских ошибок. Если вы опытный программист, можете пропустить следующий раздел про обра# ботку входных данных и перейти к разделу 8.2, который рассказывает об утверж# дениях. 8.1. Защита программы от неправильных входных данных Вы, возможно, слышали в школе выражение: «Мусор на входе — мусор на выхо# де» 1 . Это вариант предостережения потребителю от разработчиков ПО: пусть пользователь остерегается. Для промышленного ПО принцип «мусор на входе — мусор на выходе» не слишком подходит. Хорошая программа никогда не выдает мусор не# зависимо от того, что у нее было на входе. Вместо этого она использует принципы: «мусор на входе — ничего на выходе», «мусор на входе — сообщение об ошибке на выходе» или «мусор на входе не допускается». По сегодняшним стан# дартам «мусор на входе — мусор на выходе» — признак небрежного, небезопас# ного кода. Существует три основных способа обработки входных мусорных данных, пере# численные далее. Проверяйте все данные из внешних источников Получив данные из фай# ла, от пользователя, из сети или любого другого внешнего интерфейса, удосто# верьтесь, что все значения попадают в допустимый интервал. Проверьте, что чис# ловые данные имеют разрешенные значения, а строки достаточно коротки, что# бы их можно было обработать. Если строка должна содержать определенный набор значений (скажем, идентификатор финансовой транзакции или что#либо подоб# ное), проконтролируйте, что это значение допустимо в данном случае, если же нет — отклоните его. Если вы работаете над приложением, требующим соблюде# ния безопасности, будьте особенно осмотрительны с данными, которые могут атаковать вашу систему: попыткам переполнения буфера, внедренным SQL#коман# дам, внедренному HTML# или XML#коду, переполнениям целых чисел, данным, передаваемым системным вызовам и т. п. Проверяйте значения всех входных параметров метода Проверка значе# ний входных параметров метода практически то же самое, что и проверка дан# ных из внешнего источника, за исключением того, что данные поступают из дру# гого метода, а не из внешнего интерфейса. В разделе 8.5 вы узнаете, как опреде# лить, какие методы должны проверять свои входные данные. 1 «Garbage in, garbage out». Возможно, эту их школьную поговорку следует перевести нашей сту# денческой: «Каков стол, таков и стул». — Прим. перев. 184 ЧАСТЬ II Высококачественный код Решите, как обрабатывать неправильные входные данные Что делать, если вы обнаружили неверный параметр? В зависимости от ситуации вы можете выб# рать один из дюжины подходов, подробно описанных в разделе 8.3. Защитное программирование — это полезное дополнение к другим способам улучшения качества программ, описанным в этой книге. Лучший способ защит# ного кодирования — изначально не плодить ошибок. Итеративное проектирова# ние, написание псевдокода и тестов до начала кодирования и низкоуровневая проверка соответствия проекту — это все, что помогает избежать добавления де# фектов. Поэтому этим технологиям должен быть дан более высокий приоритет, чем защитному программированию. К счастью, вы можете использовать защит# ное программирование в сочетании с ними. Защита от проблем, кажущихся несущественными, может иметь большее значе# ние, чем можно подумать (рис. 8#1). В оставшейся части этой главы я расскажу о проверке данных из внешних источников, проверке входных параметров и об# работке неправильных входных данных. Рис. 8'1. Часть плавучего моста Interstate%90 в Сиэтле затонула во время шторма, потому что резервуары были оставлены открытыми. Они наполнились водой, и мост стал слишком тяжел, чтобы держаться на плаву. Обеспечение защиты от мелочей во время проектирования может значить больше, чем кажется 8.2. Утверждения Утверждение (assertion) — это код (обычно метод или макрос), используемый во время разработки, с помощью которого программа проверяет правильность сво# его выполнения. Если утверждение истинно, то все работает так, как ожидалось. Если ложно — значит, в коде обнаружена ошибка. Например, если система пред# полагает, что длина файла с информацией о заказчиках никогда не будет превы# шать 50 000 записей, программа могла бы содержать утверждение, что число за# писей меньше или равно 50 000. Пока это число меньше или равно 50 000, утвер# |