Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 8 Защитное программирование 193 8.4. Исключения Исключения — это специальное средство, позволяющее передать в вызывающий код возникшие ошибки или исключительные ситуации. Если код в некотором методе встречает неожиданную ситуацию и не знает, как ее обработать, то он генерирует исключение, т. е. фактически умывает руки со словами: «Я не знаю, что с этим делать, надеюсь, кто-нибудь другой знает, как на это реагировать!» Код, не имеющий понятия о контексте ошибки, может вернуть управление другой части системы, которая, возможно, лучше знает, как интерпретировать ошибку и сде- лать с ней что-то осмысленное. Кроме того, исключения могут быть полезны для упрощения запутанной логики участка кода, как в примере «Переписатьс помощью try-finally» в разделе 17.3. Вот принцип действия исключений: метод, применяя оператор throw, создает объект- исключение. Код какого-либо другого метода, стоящего выше в иерархии вызо- вов, перехватит это исключение в блоке try-catch. Популярные языки программирования по-разному реализуют исключения (табл. 8.1): Табл. 8-1. Поддержка исключений в популярных языках программирования Параметры обработки исключений C++ Java Visual Basic Поддержка try-catch Да. Да. Да. Поддержка Нет. Да. Да. try-catch-finally Что генерируется Объект класса Excep- Объект класса Объект класса tion или производно- Exception или про- Exception или про- го от него, указатель изводного от него. изводного от него. на объект, объектная ссылка, другие типы данных, например, строка или целое число. Эффект при Вызывается функция Если это «проверяе- Программа не перехваченном std::unexpected(), кото- мое исключение», завершает работу. исключении рая по умолчанию вы- то прекращается зывает std::terminate(), работа потока, в ко- в свою очередь по умол- тором оно возникло. чанию вызывающая Если это «исключе- функцию abort(). ние периода выпол- нения», то оно игнорируется. Генерируемые Нет. Да. Нет. исключения должны быть определены в интерфейсе класса Перехватываемые Нет. Да. Нет. исключения должны быть определены в интерфейсе класса 194 ЧАСТЬ II Высококачественный код Исключения и наследование имеют общее свойство: исполь- зуемые разумно, они могут уменьшить сложность. Исполь- зуемые чрезмерно, они могут сделать код абсолютно нечи- таемым. Этот раздел содержит предложения по реализации преимуществ исключений и способы избежать трудностей, которые часто с ними связаны. Используйте исключения для оповещения других час- тей программы об ошибках, которые нельзя игно- рировать Основное преимущество исключений состоит в их способности сигнализировать об ошибке так, что ее нельзя проигнориро- вать (Meyers, 1996). При других подходах к обработке ошибок есть вероятность, что сбойная ситуация останется незамеченной. Исключения устраняют такую возможность. Генерируйте исключения только для действительно исключительных ситуаций Применение исключений должно быть зарезервировано только для действительно исключительных случаев — иначе говоря, для ситуаций, которые нельзя реализовать другими методами кодирования. Исключения используются в таких же обстоятельствах, как и утверждения: для событий, которые не просто редко происходят, а которые никогда не должны случаться. Исключения представляют собой компромисс между возможностью обработки непредвиденных ситуаций, с одной стороны, и повышением сложности — с дру- гой. Исключения ухудшают инкапсуляцию, требуя от кода, вызывающего метод, знать, какие исключения могут быть сгенерированы внутри него. Это усложняет код, что противоречит Главному Техническому Императиву ПО (см. главу 5), суть которого в снижении сложности. Не используйте исключения по мелочам Если ошибка может быть обрабо- тана локально, там ее и обрабатывайте. Не генерируйте в коде неперехватывае- мое исключение, если ошибка может быть исправлена на месте. Избегайте генерировать исключения в конструкторах и деструкторах, если только вы не перехватываете их позднее Правила обработки исклю- чений очень быстро усложняются, когда исключения генерируются в конструк- торах и деструкторах. Так, в C++ деструктор не вызывается, пока объект не создан полностью. Это значит, что, если код в конструкторе сгенерировал исключение, деструктор вызван не будет, что приведет к возможной утечке ресурсов (Meyers, 1996; Stroustrup, 1997). Аналогичные замысловатые правила относятся и к исклю- чениям внутри деструкторов. Приверженцы языка могут сказать, что запомнить эти правила очень легко. Но все же программисты — простые смертные, у них могут возникнуть трудности с за- поминанием правил. Наилучшая программистская практика — избегать излишней сложности, которая создается при написании такого кода. Генерируйте исключения на правильном уровне аб- стракции Интерфейс метода и класса должен представ- лять собой целостную абстракцию. Генерируемые исключе- ния — такая же часть интерфейса, как и специальные типы данных. Программы, использующие ис- ключения как часть нормальной работы алгоритма, страдают от всех проблем с читабельностью и удобством сопровождения так же, как и классический спагет- ти-код. Энди Хант и Дэйв Томас (Andy Hunt and Dave Thomas) Перекрестная ссылка О поддер- жании целостных абстракций интерфейса см. подраздел «Хо- рошая абстракция» раздела 6.2. ГЛАВА 8 Защитное программирование 195 Решив передать исключение, удостоверьтесь, что уровень абстракции исключения и метода совпадают. Вот как не надо делать: Плохой пример Java-класса, который генерирует исключение на неверном уровне абстракции class Employee { Объявление исключения с неправильным уровнем абстракции. public TaxId GetTaxId() throws EOFException { } } Функция GetTaxId() передает низкоуровневое исключение EOFException вызыва- ющей стороне. Она не обрабатывает исключение сама, а раскрывает некоторые детали своей реализации, генерируя низкоуровневое исключение. Это привязы- вает клиентский код не к классу Employee, а к коду внутри класса Employee, гене- рирующему исключение EOFException. Инкапсуляция нарушена, управляемость кода ухудшается. Вместо этого код GetTaxId() должен передавать исключение, соответствующее интерфейсу класса, частью которого он является, например, так: Хороший пример Java-класса, который генерирует исключение на правильном уровне абстракции class Employee { Объявление исключения, соответствующего уровню абстракции. public TaxId GetTaxId() throws EmployeeDataNotAvailable { } } Код обработки исключений внутри GetTaxId(), возможно, просто устанавливает соответствие между исключениями io_disk_not_ready и EmployeeDataNotAvailable, что гораздо лучше, так как сохраняется абстракция интерфейса. Вносите в описание исключения всю информацию о его причинах Каж- дое исключение возникает при определенных обстоятельствах, обнаруженных ко- дом в момент генерации этого исключения. Эти сведения недоступны тому, кто читает сообщение об исключении. Убедитесь, что это сообщение содержит до- статочно информации для понимания причины генерации исключения. Напри- мер, если причиной был неправильный индекс элемента массива, включите в опи- сание верхнюю и нижнюю границы массива и некорректное значение индекса. > > 196 ЧАСТЬ II Высококачественный код Избегайте пустых блоков catch Иногда возникает искушение оставить без внимания исключение, которое вы не знаете, как обработать. Например: Плохой пример игнорирования исключения (Java) try { // много кода } catch ( AnException exception ) { } Такой подход говорит о том, что либо код внутри блока try генерирует исключе- ние без причины, либо код в блоке catch не обрабатывает возможную исключи- тельную ситуацию. Выясните, в чем суть проблемы, и исправьте блоки try или catch. Изредка можно столкнуться с ситуацией, когда исключение более низкого уровня не соответствует уровню абстракции вызывающего метода. В этом случае хотя бы задокументируйте, почему блок catch должен быть пустым. Это можно сделать в комментариях или записав сообщение в файл журнала, например: Хороший пример игнорирования исключения на Java try { // много кода } catch ( AnException exception ) { LogError( ”Unexpected exception” ); } Выясните, какие исключения генерирует используемая библиотека Ес- ли вы работаете с языком, не требующим, чтобы метод или класс объявляли воз- можные исключения, убедитесь, что вам известно, какие исключения могут воз- никнуть в коде используемых библиотек. Неперехваченное исключение из биб- лиотеки приведет к аварийному завершению программы так же легко, как и исключение, сгенерированное в вашем коде. Если библиотечные исключения не документированы, создайте код-прототип и протестируйте библиотеки, чтобы их выявить. Рассмотрите вопрос о централизованном выводе информации об исклю- чениях Поддержание целостности в обработке исключений обеспечивает цен- трализованный генератор сообщений об исключениях. Он содержит базу знаний о том, какие это исключения, как каждое из них должно быть обработано, каков формат их сообщений и т. п. Вот пример упрощенного обработчика исключений. Он просто печатает диагно- стическое сообщение: ГЛАВА 8 Защитное программирование 197 Пример централизованного генератора сообщений об исключениях, часть 1 (Visual Basic) Sub ReportException( _ ByVal className, _ ByVal thisException As Exception _ ) Dim message As String Dim caption As String message = ”Exception: ” & thisException.Message & ”.” & ControlChars.CrLf & _ ”Class: ” & className & ControlChars.CrLf & _ ”Routine: ” & thisException.TargetSite.Name & ControlChars.CrLf caption = ”Exception” MessageBox.Show( message, caption, MessageBoxButtons.OK, _ MessageBoxIcon.Exclamation ) End Sub Этот обработчик можно использовать следующим образом: Пример централизованного генератора сообщений об исключениях, часть 2 (Visual Basic) Try Catch exceptionObject As Exception ReportException( CLASS_NAME, exceptionObject ) End Try Код этой версии ReportException() несложен. В реальных приложениях вы може- те сделать отчет настолько кратким или подробным, насколько это необходимо в вашем обработчике исключений. Если вы решили создать централизованный генератор сообщений, примите во вни- мание основные проблемы с централизованной обработкой ошибок, обсуждаемые в подразделе «Вызвать процедуру или объект — обработчик ошибок» раздела 8.3. Стандартизуйте использование исключений в вашем проекте Чтобы со- хранить процедуру обработки исключений максимально интеллектуально управ- ляемой, вы можете стандартизовать использование исключений несколькими спо- собами. 쐽 Если вы работаете с языком, таким как C++, который позволяет генерировать исключения разных типов, стандартизуйте, что конкретно будет создаваться. В целях совместимости с другими языками подумайте об использовании только объектов, порожденных от базового класса Exception. 쐽 Подумайте о создании собственного класса исключений, который может слу- жить базовым классом для всех исключений, возникающих в вашем проекте. Это поможет централизовать и стандартизовать регистрацию, обработку и другие действия с ошибками. Дополнительные сведения Об этой технологии см. «Practical Standards for Microsoft Visual Basic .NET» (Foxall, 2003). 198 ЧАСТЬ II Высококачественный код 쐽 Определите конкретные случаи, в которых код может использовать синтаксис throw-catch для локальной обработки ошибок. 쐽 Определите конкретные случаи, в которых код может сгенерировать исклю- чение, не перехватываемое локально. 쐽 Решите, будет ли использоваться централизованный генератор сообщений об исключениях. 쐽 Определите, допускаются ли исключения в конструкторах и деструкторах. Рассмотрите альтернативы исключениям Некоторые языки поддерживают исключения 5–10 лет и более. Одна- ко до сих пор нет общепринятых правил их безопасного использования. Некоторые программисты применяют исключения для обработки ошибок толь- ко потому, что их язык программирования предоставляет такой механизм. Вам все- гда следует принимать во внимание все возможные методы обработки ошибок: локальную обработку ошибок, возврат кода ошибки, запись отладочной инфор- мации в файл, прекращение работы системы и др. Обрабатывать ошибки с помо- щью исключений только потому, что это позволяет язык, — классический пример программирования на языке, а не с использованием языка (см. разделы 4.3 и 34.4). И напоследок подумайте, действительно ли вашей программе необходимо обра- батывать исключения. Точка. Как заметил Бьерн Страуструп, иногда лучшей реак- цией на серьезную ошибку периода выполнения будет освобождение всех ресур- сов и прекращение работы. Пусть пользователь перезапустит программу с надле- жащими входными данными (Stroustrup, 1997). 8.5. Изоляция повреждений, вызванных ошибками Изоляция повреждений, или баррикада, — это стратегия, сходная с тем, как изо- лируются отсеки в трюме корабля. Если корабль налетает на айсберг и в днище появляется пробоина, отсеки задраиваются, и остальная часть корабля не страда- ет. Баррикады также аналогичны брандмауэрам, предотвращающим распростра- нение огня из одной части здания в другую. (Ранее баррикады и назывались бран- дмауэрами, но сейчас термин «брандмауэр» обычно относится к блокировке не- желательного сетевого трафика.) Один из способов изоляции в целях защитного программирования состоит в разработке набора интерфейсов в качестве оболочки для «безопасных» частей кода. Проверяйте корректность данных, пересекающих границу безопасной области, и реагируйте соответственно, если данные неправильные (рис. 8-2). Перекрестная ссылка Об альтер- нативных подходах к обработ- ке ошибок см. раздел 8.3. ГЛАВА 8 Защитное программирование 199 Рис. 8-2. Выделение части кода для работы с непроверенными данными, а части — для работы с только корректными данными, может быть эффективным способом освободить большую часть программы от ответственности за проверку допустимости данных Тот же подход применим и на уровне класса. Открытые методы класса предпола- гают, что данные небезопасны и отвечают за их проверку и исправление. Если данные были проверены открытыми методами класса, закрытые методы могут считать, что данные безопасны. Этот подход можно представить и такой аналогией: данные стерилизуются, прежде чем войти в операционную. Все, что находится в ней, считается безопасным. Клю- чевой вопрос проектирования — решить, что должно быть в операционной, что — остаться снаружи и где быть дверям. Иначе говоря, какие методы поместить внутри безопасной зоны, какие — снаружи, а какие будут проверять данные. Про- стейший способ — проверка внешних данных по мере их поступления. Но ин- формацию часто необходимо проверять неоднократно, на нескольких уровнях, поэтому иногда требуется многоуровневая стерилизация. Преобразовывайте входные данные к нужному типу в момент ввода Данные на входе обычно представлены в форме строки или числа. Иногда значе- ние соответствует булевому типу, например, «да» или «нет». Иногда — перечисли- мому, скажем, Color_Red, Color_Green, и Color_Blue. Сохранение данных неопреде- ленного типа в течение неизвестного периода времени усложняет программу и увеличивает шансы, что кто-нибудь может вывести программу из строя, указав «Да» в качестве цвета. Преобразуйте входные данные в надлежащую форму как можно раньше. 200 ЧАСТЬ II Высококачественный код Связь между баррикадами и утверждениями Применение баррикад делает отчетливым различие между утверждениями и об- работкой ошибок. Методы с внешней стороны баррикады должны использовать обработчики ошибок, поскольку небезопасно делать любые предположения о данных. Методы внутри баррикад должны использовать утверждения, так как дан- ные, переданные им, считаются проверенными при прохождении баррикады. Если один из методов внутри баррикады обнаруживает некорректные данные, это сле- дует считать ошибкой в программе, а не в данных. Использование баррикад также иллюстрирует значимость принятия решения об обработке ошибок на уровне архитектуры. Решение, какой код находится внут- ри, а какой — снаружи баррикады, принимается на уровне архитектуры. 8.6. Отладочные средства Еще один ключевой аспект защитного программирования — отладка, способная стать могучим союзником в быстром обнаружении ошибок. Не применяйте ограничения промышленной версии к отладочной версии автоматически Известным заблуждением программистов является предпо- ложение, что ограничения промышленной версии относятся и ко времени разработки. Промышленная версия должна работать быстро. Отладочная — может себе позволить ра- ботать медленно. Промышленная версия должна быть эко- номна с ресурсами. Отладочная — может быть расточитель- ной. Промышленная версия не должна позволять пользователю делать опасные действия. Отладочная — может предоставлять дополнительные возможности без риска нарушить безопасность. Одна моя программа интенсивно использовала четырехсвязный список. Код это- го списка содержал ошибки, и иногда список повреждался. Я добавил пункт меню для проверки целостности этого связного списка. В отладочном режиме Microsoft Word содержит код, который в момент простоя каждые несколько секунд проверяет целостность объекта Document. Это помогает быстро обнаруживать повреждение данных, что упрощает диагностику ошибок. Будьте готовы поступиться скоростью и ресурсоемкостью во время раз- работки в обмен на встроенные средства, позволяющие процессу разра- ботки двигаться более гладко. Внедрите поддержку отладки как можно раньше Чем раньше вы добавите отладочные средства, тем больше они помогут. Обычно вы не добавляете отладочную информацию, пока несколько раз не столкнетесь с про- блемой. Если же вы внедрите поддержку отладки после первого раза или перенесете ее из предыдущего проекта, она будет помогать вам на протяжении всей работы. Дополнительные сведения Об использовании отладочного ко- да в защитном программирова- нии см. «Writing Solid Code» (Maguire, 1993). |