Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 8 Защитное программирование 201 Используйте наступательное программирование Исключительные случаи должны обрабатываться так, что- бы во время разработки они были очевидны, а в промыш- ленном коде — позволяли продолжить работу. Майкл Ховард и Дэвид Леблан назвали этот подход «наступательным про- граммированием» (Howard and LeBlanc, 2003). Допустим, у вас есть оператор case, который, как вы ожида- ете, будет обрабатывать только 5 видов событий. Во время разработки вариант по умолчанию нужно использовать для генерации предупреждения «Эй! Здесь еще один вариант! Исправьте программу!». Однако в промышленной версии реакция в этом случае должна быть более вежливой. Можно, например, делать запись в журнал ошибок. Вот некоторые приемы наступательного программирования. 쐽 Убедитесь, что утверждения завершают работу програм- мы. Нельзя, чтобы у программистов вошло в привычку просто нажимать клавишу Enter для пропуска уже изве- стной проблемы. Сделайте проблему достаточно мучи- тельной, чтобы ее исправили. 쐽 Заполняйте любую выделенную память, чтобы можно было обнаружить ошибки выделения памяти. 쐽 Заполняйте все файлы и потоки, чтобы выявить ошибки файловых форматов. 쐽 Убедитесь, что при попадании в ветвь default или else всех операторов case программа прекращает работу или еще как-то заставляет обратить на это вни- мание. 쐽 Заполняйте объекты мусором перед самым их удалением. 쐽 Настройте программу на отправку вам журналов ошибок по электронной по- чте, чтобы видеть, какие ошибки происходят в рабочем ПО, если, конечно, это можно сделать в ваших программах. Иногда нападение — лучшая защита. Чем жестче требования во время разработ- ки, тем проще эксплуатация программы. Запланируйте удаление отладочных средств Если вы пишете код для себя, возможно, было бы хорошо оставить всю отладоч- ную информацию в программе. Но в коде для коммерческого использования тре- бования к скорости и размеру скорее всего этого не допустят. Решите заранее, как избежать постоянного перетаскивания отладочного кода в программу и из нее. Вот несколько способов сделать это: Для контроля версий и сборки программы используй- те инструменты ant и make Средства контроля версий позволяют создавать варианты программы из одних и тех же исходных файлов. А инструменты для сборки позволяют вам настроить вклю- чение отладочного кода в режиме разработки и его исключение в коммерческой версии. Перекрестная ссылка Об обра- ботке непредвиденных случаев см. подраздел «Советы по ис- пользованию операторов case» раздела 15.2. Неработающая программа обыч- но приносит меньше вреда, чем работающая плохо. Энди Хант и Дэйв Томас (Andy Hunt and Dave Thomas) Перекрестная ссылка О контро- ле версий см. раздел 28.2. 202 ЧАСТЬ II Высококачественный код Используйте встроенный препроцессор Если в вашей программной среде есть препроцессор, как, например, в C++, вы можете добавлять или удалять отладоч- ный код простым изменением параметра компиляции. Препроцессор можно за- действовать непосредственно, а можно писать макросы, работающие с его опре- делениями. Вот пример написания кода, напрямую использующего препроцессор: Пример непосредственного использования препроцессора для управления отладочным кодом (C++) Для добавления кода отладки используйте директиву #define, чтобы определить символ DEBUG. Для исключения отладочного кода просто не определяйте DEBUG. #define DEBUG #if defined( DEBUG ) // отладочный код #endif У этой темы несколько вариаций. Вместо простого определения DEBUG вы може- те присвоить ему значение, а затем проверять именно это значение, а не просто факт определения символа. Так вы можете различать несколько уровней отладоч- ного кода. Какой-то код вы хотели бы использовать в программе все время, по- этому вы окружаете его операторами вроде #if DEBUG > 0. Другой отладочный код может понадобиться только в специальных целях, и вы можете заключить его в операторы #if DEBUG == POINTER_ERROR. В других местах вы захотите установить различные уровни отладки, для чего годятся такие выражения, как #if DEBUG > LEVEL_A. Если вы не хотите распространять #if defined() по всему коду, можно написать макрос препроцессора, выполняющий ту же задачу. Вот его пример: Пример использования макроса препроцессора для управления отладочным кодом на C++ #define DEBUG #if defined( DEBUG ) define DebugCode( code_fragment ) { code_fragment } else #define DebugCode( code_fragment ) #endif DebugCode( Этот код добавляется или удаляется в зависимости от того, определен ли символ DEBUG. statement 1; statement 2; statement n; > > ГЛАВА 8 Защитное программирование 203 ); Как и в первом примере применения препроцессора, эта технология может быть изменена различными способами, что позволит выполнить более изощренные действия, чем полное включение или исключение отладочного кода. Напишите собственный препроцессор Если язык не содержит препроцессор, то для включения/исключения от- ладочного кода довольно легко написать свой. Разработай- те правила для добавления отладочного кода и напишите свой прекомпилятор, следующий этим соглашениям. Скажем, в Java вы могли бы написать прекомпилятор для обработ- ки ключевых слов //#BEGIN DEBUG и //#END DEBUG. Напишите сценарий для вы- зова препроцессора, а затем скомпилируйте полученный после него код. В дол- госрочной перспективе вы сбережете время. Кроме того, вы не сможете случай- но скомпилировать необработанный код. Используйте отладочные заглушки Зачастую для вы- полнения отладочных проверок вы можете вызвать проце- дуру. Во время разработки она могла бы выполнять несколь- ко операций перед тем, как управление вернется вызывающей стороне. В промыш- ленном коде сложную процедуру можно заменить процедурой-заглушкой, кото- рая сразу вернет управление или выполнит перед этим пару быстрых операций. Этот подход лишь немного снижает производительность и является более быст- рым решением, чем написание собственного препроцессора. Храните и отладоч- ную, и итоговую версии процедур, и вы сможете быстро переключаться между ними. Вы можете начать с метода, разработанного для проверки переданных ему указа- телей: Пример метода, использующего отладочную заглушку (C++) void DoSomething( SOME_TYPE *pointer; ) { // проверка переданных сюда параметров Это строка вызывает процедуру для проверки указателя. CheckPointer( pointer ); } Во время разработки процедура CheckPointer() будет выполнять полную провер- ку указателя. Это будет медленно, но эффективно, например, так: Перекрестная ссылка О препро- цессорах и источниках инфор- мации об их написании см. под- раздел «Препроцессоры» разде- ла 30.3. Перекрестная ссылка О заглуш- ках см. раздел 22.5. > 204 ЧАСТЬ II Высококачественный код Пример метода, проверяющего указатели во время разработки (C++) Этот метод проверяет любой переданный ему указатель. Во время разработки ее можно исполь- зовать для стольких проверок, сколько вы сможете выдержать. void CheckPointer( void *pointer ) { // выполнить проверку 1 — например, что указатель не равен NULL // выполнить проверку 2 — например, что какой-то его // обязательный признак действителен // выполнить проверку 3 — например, что область, // на которую он указывает, не повреждена // выполнить проверку n—... } Когда код готов к эксплуатации, вас, возможно, не устроят накладные расходы, связанные с такой проверкой указателей. Тогда вы удалите предыдущий код и добавьте следующий метод: Пример метода, проверяющего указатели во время эксплуатации (C++) Эта процедура сразу же возвращает управление. void CheckPointer( void *pointer ) { // никакого кода; просто возврат управления } Это отнюдь не исчерпывающий обзор всех способов удаления средств отладки. Но его должно быть достаточно, чтобы подать вам идею о том, что может рабо- тать в вашей программной среде. 8.7. Доля защитного программирования в промышленной версии Один из парадоксов защитного программирования состоит в том, что во время разработки вы бы хотели, чтобы ошибка была заметной: лучше пусть она надое- дает, чем будет существовать риск ее пропустить. Но во время эксплуатации вы бы предпочли, чтобы ошибка была как можно более ненавязчивой, чтобы про- грамма могла элегантно продолжить или прекратить работу. Далее перечислены основные принципы для определения, какие инструменты защитного програм- мирования следует оставить в промышленной версии, а какие — убрать. Оставьте код, которые проверяет существенные ошибки Решите, какие части программы могут содержать скрытые ошибки, а какие — нет. Скажем, раз- рабатывая электронную таблицу, вы можете скрывать ошибки, касающиеся обнов- ления экрана, так как в худшем случае это приведет к неправильному изображе- нию. А вот в вычислительном модуле скрытых ошибок быть не должно, посколь- ку такие ошибки могут привести к неверным расчетам в электронной таблице. Большинство пользователей предпочтут помучиться с некорректным выводом на экран, чем с неправильным расчетом налогов и аудитом налоговой службы. > > ГЛАВА 8 Защитное программирование 205 Удалите код, проверяющий незначительные ошибки Если последствия ошибки действительно незначительны, удалите код, который ее проверяет. В на- шем примере вы могли бы удалить код, проверяющий обновление экрана элект- ронной таблицы. При этом «удалить» значит не физически убрать код, но исполь- зовать управление версиями, переключатели прекомпилятора или другую техно- логию для компиляции программы без этого кода. Если занимаемое место несу- щественно, проверочный код можно оставить, но настроив для ненавязчивой за- писи сообщений в журнал ошибок. Удалите код, приводящий к прекращению работы программы Как я уже говорил, если на стадии разработки ваша программа обнаруживает ошибку, ее надо сделать позаметнее, чтобы ее могли исправить. Часто наилучшим действием при выявлении ошибки будет печать диагностического сообщения и прекращение работы. Это полезно даже для незначительных ошибок. Во время эксплуатации пользователям нужна возможность сохранения своей ра- боты, прежде чем программа рухнет. И поэтому они, вероятно, будут согласны терпеть небольшие отклонения в обмен на поддержание работоспособности программы на достаточное для сохранения время. Пользователи не приветству- ют ничего, что приводит к потере результатов их работы, независимо от того, насколько это помогает отладке и в конце концов улучшает качество продукта. Если ваша программа содержит отладочный код, способный привести к потере данных, уберите его из промышленной версии. Оставьте код, который позволяет аккуратно завершить работу про- граммы Если программа содержит отладочный код, определяющий потенциально фатальные ошибки, оставьте его — это позволит элегантно завершить работу. На- пример, в марсоходе Pathfinder инженеры намеренно оставили часть отладочно- го кода. Ошибка произошла после того, как Pathfinder совершил посадку. С помо- щью отладочных средств, оставленных в нем, инженеры из лаборатории реактив- ных двигателей смогли диагностировать проблему и загрузить исправленный код. В результате Pathfinder полностью выполнил свою миссию (March, 1999). Регистрируйте ошибки для отдела технической поддержки Обдумайте возможность оставить отладочные средства в промышленной версии, но изменить их поведение на более подходящее. Если вы заполнили ваш код утверждениями, прекращающими выполнение программы на стадии разработки, на стадии экс- плуатации можно не удалять их совсем, а настроить процедуру утверждения на запись сообщений в файл. Убедитесь, что оставленные сообщения об ошибках дружелюбны Если вы оставляете в программе внутренние сообщения об ошибках, проверьте, что они дружественны к пользователю. Пользователь одной из моих первых программ сообщила мне, что получила сообщение, гласившее: «У тебя неправильно выделе- на память для указателя, черт возьми!» К счастью для меня, у нее было чувство юмора. Общепринятым и эффективным подходом является уведомление пользователя о «внутренней ошибке» и вывод телефона и адреса электронной почты, по которым о ней можно сообщить. 206 ЧАСТЬ II Высококачественный код 8.8. Защита от защитного программирования Избыток защитного программирования сам по себе созда- ет проблемы. Если вы проверяете данные, передаваемые через параметры, во всех возможных местах и всеми воз- можными способами, ваша программа будет слишком боль- шой и медленной. Еще хуже то, что дополнительный код, необходимый для защитного программирования, увеличивает сложность. Напи- санный в этих целях код не лишен недостатков, и вы можете находить дефекты в коде защитного программирования, так же как и в обычном коде, особенно если вы добавляете его мимоходом. Подумайте, где надо защищаться, и соответствен- но расставьте приоритеты защитного программирования. Контрольный список: защитное программирование Общие Реализована ли в методе защита от некорректных вход- ных данных? Используете ли вы утверждения для документирования допущений, вклю- чая пред- и постусловия? Используются ли утверждения для документирования только тех условий, которые никогда не должны происходить? Определены ли в архитектуре или высокоуровневом проекте системы тех- нологии обработки ошибок? Указано ли в архитектуре или высокоуровневом проекте системы, чему будет отдаваться предпочтение при обработке ошибок: устойчивости или коррек- тности? Построены ли баррикады для изоляции разрушительного эффекта ошибок и уменьшения объема кода, занятого в обработке ошибок? Установлены ли отладочные средства таким образом, что их можно активи- зировать и деактивировать без особых проблем? Хватает ли защитного кода: не слишком много и не слишком мало? Использованы ли технологии наступательного программирования, чтобы затруднить пропуск ошибок на стадии разработки? Исключения Определен ли в проекте стандартизованный подход к обработке исключений? Рассмотрены ли альтернативы использованию исключений? Обрабатывается ли ошибка по возможности локально или генерируется нелокальное исключение? Возможны ли исключения в конструкторах и деструкторах? Генерируются ли исключения в методах на подходящих уровнях абстракции? Содержит ли каждое исключение все относящиеся к нему исходные данные? Свободен ли код от пустых блоков catch? (Или, если блок catch действи- тельно допустим, задокументировано ли это?) Слишком много чего-либо — это плохо, но слишком много вис- ки — это просто достаточно. Марк Твен http://cc2e.com/0868 ГЛАВА 8 Защитное программирование 207 Вопросы безопасности Действительно ли код, проверяющий некорректные входные данные, конт- ролирует попытки переполнения буфера, внедрения SQL- и HTML-кода, пе- реполнения целых чисел и других злонамеренных действий? Все ли ошибочные коды возврата проверяются? Все ли исключения перехватываются? Не содержат ли сообщения об ошибках информацию, которая может помочь злоумышленнику взломать систему? Дополнительные ресурсы Просмотрите следующие ресурсы по защитному програм- мированию. Безопасность Howard, Michael, and LeBlanc David. Writing Secure Code, 2d ed. Redmond, WA: Microsoft Press, 2003. Авторы обсуждают важность вопроса доверия ко входным данным для безопасности системы. Книга открывает глаза на то, какими многочисленными способами может быть нарушена работа программы. Некоторые из них связаны с проектированием, но большинство — нет. Книга охватывает весь диапазон воп- росов о требованиях к системе, проектировании, кодировании и тестировании. Утверждения Maguire, Steve. Writing Solid Code. Redmond, WA: Microsoft Press, 1993. Глава 2 содер- жит отличное обсуждение вопросов применения утверждений, в том числе несколь- ко интересных примеров утверждений из широко известных продуктов Microsoft. Stroustrup, Bjarne. The C++ Programming Language, 3d ed. Reading, MA: Addison-Wesley, 1997. В разделе 24.3.7.2 описано несколько вариантов реализации утверждений в C++, включая взаимосвязь утверждений и пред- и постусловий. Meyer, Bertrand. Object-Oriented Software Construction, 2d ed. New York, NY: Prentice Hall PTR, 1997. Эта книга содержит наиболее полное обсуждение пред- и постусловий. Исключения Meyer, Bertrand. Object-Oriented Software Construction, 2d ed. New York, NY: Prentice Hall PTR, 1997. Глава 12 содержит подробное обсуждение обработки исключений. Stroustrup, Bjarne. The C++ Programming Language, 3d ed. Reading, MA: Addison-Wesley, 1997. В главе 14 подробно обсуждается обработка исключений на C++. Раздел 14.11 содержит отличное резюме, состоящее из 21 совета по использованию исключе- ний в C++. Meyers, Scott. More Effective C++: 35 New Ways to Improve Your Programs and Designs. Reading, MA: Addison-Wesley, 1996. В темах 9–15 описаны разнообразные нюансы по обработке исключений в C++. Arnold, Ken, James Gosling, and David Holmes. The Java Programming Language, 3d ed. Boston, MA: Addison-Wesley, 2000. Глава 8 содержит обсуждение обработки ис- ключений на Java. http://cc2e.com/0875 208 ЧАСТЬ II Высококачественный код Bloch, Joshua. Effective Java Programming Language Guide. Boston, MA: Addison-Wesley, 2001. В темах 39–47 описаны нюансы обработки исключений на Java. Foxall, James. Practical Standards for Microsoft Visual Basic .NET. Redmond, WA: Microsoft Press, 2003. В главе 10 описана обработка исключений на Visual Basic. Ключевые моменты 쐽 Промышленный код должен обрабатывать ошибки более изощренно, чем по принципу «мусор на входе — мусор на выходе». 쐽 С помощью технологии защитного программирования ошибки легче находить, легче исправлять, и они наносят меньше вреда промышленному коду. 쐽 Утверждения позволяют обнаружить ошибки на ранней стадии, особенно в больших системах, системах повышенной надежности и в системах с часто изменяемым кодом. 쐽 Выбор способа обработки некорректных входных данных — это ключевое решение обработки ошибок, принимаемое на этапе высокоуровневого проек- тирования. 쐽 Исключения предоставляют возможность обработки ошибок в измерении, отличном от нормального хода алгоритма. Если они используются с осторож- ностью, то являются важным дополнением в интеллектуальном инструменталь- ном наборе программиста. Применять их следует после сравнения с другими технологиями обработки ошибок. 쐽 Ограничения, применяемые к промышленной версии системы, не обязатель- но должны относиться и ко времени разработки. Пользуясь этим преимуще- ством, вы можете добавлять в отладочную версию любой код, помогающий быстро выявлять ошибки. |