В больших программах, особенно в тех, которые предназначены для управления устройствами, которые потенциально могут представлять угрозу для жизни, любая возможность возникновения ошибки должна быть сведена к минимуму. Маленькие программы еще могут быть верифицированы (тщательно проверены на отсутствие ошибок), но с большими программами такая работа вряд ли осуществима. (Верифицированная программа не содержит ошибок и никогда не допускает сбоев в работе, по крайней мере, теоретически.) К примеру, представьте себе программу, которая управляет закрылками современного реактивного самолета. Физически невозможно протестировать все возможные взаимодействия сил, которые могут быть приложены к самолету. Это означает, что вы не можете в полной мере протестировать программу. В лучшем случае, максимум, что можно будет сказать о ней — она работает корректно в таких-то условиях и при таких-то обстоятельствах. В программах подобного рода вы (как пассажир или программист) вряд ли захотели бы столкнуться с крушением (программы или самолета)!
После программирования в течение нескольких лет вы заметите, что хотя большинство отказов в работе программы происходят по причине самых разных ошибок программирования, само количество типов этих ошибок сравнительно невелико. Например, многие так называемые катастрофические программные сбои вызваны одной из следующих довольно частых ошибок:
- Какое-то условие привело к непредусмотренному выполнению бесконечного цикла;
- Были нарушены границы массива, что привело к повреждению примыкающего к нему кода программы или данных;
- Неожиданное переполнение при обработке данных определенного типа.
Теоретически при наличии необходимого опыта программирования ошибок подобного рода можно избежать в процессе тщательного продумывания и внимательной реализации проекта. (И в самом деле, профессионально написанные программы не должны содержать ошибок подобного рода.)
Тем не менее, после первого этапа разработки программ часто появляются ошибки иного типа, проявляющиеся или во время заключительного процесса «тонкой настройки», или на стадии сопровождения программы. Такие ошибки возникают из-за того, что одна функция по непредусмотренным причинам влияет на код или данные другой функции. Подобные ошибки обнаружить исключительно тяжело, поскольку код, обеих функций может оказаться вполне корректным, а появление ошибки вызывает именно взаимодействие этих функций. Поэтому вполне естественно, что стремление уменьшить вероятность появления катастрофических сбоев приводит к желанию сделать свои функции и их данные как можно более «пуленепробиваемыми». Наилучший способ достижения этой цели состоит в том, чтобы код и данные каждой функции были скрыты как друг от друга, так и от остальной части программы.
Методы сокрытия кода и данных во многом аналогичны механизму передачи секретной информации только тем лицам, кому ее необходимо знать. Проще говоря, если какая-то функция не должна знать что-либо о другой функции или переменной, не предоставляйте этой функции возможности доступа к ним. Для этого необходимо придерживаться следующих четырех правил-принципов:
- Каждый функциональный элемент должен иметь только одну точку входа и одну точку выхода;
- Везде, где только возможно, не используйте глобальных переменных, а явно передавайте информацию функциям (например, посредством параметров);
- В случаях, когда в нескольких зависимых функциях используются глобальные переменные, необходимо размещать как эти переменные, так и функции в обособленном файле. К тому же, в таких случаях глобальные переменные должны быть объявлены как static;
- Каждая функция должна сообщать вызвавшей ее программе об успешном или аварийном завершении намеченной ей операции. То есть вызывающая программа должна распознавать признаки успешного или сбойного завершения функции.
Правило 1 устанавливает, что каждая выполняемая функция имеет только одну точку входа и одну точку выхода. Это означает, что хотя в функциональный элемент может входить несколько функций, остальная часть программы взаимодействует только с одной из них. Обратимся к программе, обрабатывающей список рассылки; эта программа обсуждалась в предыдущих разделах. В ней предусмотрено выполнение семи функций. Можно поместить каждую функцию, вызываемую из соответствующего функционального поля, в свой собственный файл и компилировать их все раздельно друг от друга. Если все будет сделано надлежащим образом, то единственной точкой входа и выхода каждого функционального элемента будет его функция наиболее высокого уровня. А применительно к программе, обрабатывающей список рассылки, это означает, что такие высокоуровневые функции будут вызываться только функцией main(), тем самым будет предотвращено случайное разрушение одного функционального элемента другим. Эту ситуацию поясняет рис.27.1.
.---------------+------+--------------.
| .---------|main()|--------. |
| | .---+--+---+-. | |
| | | | | | |
| | | | | | |
+---V---+ | +---V----+ | +---V----+ | +---V---+
|Функция| | |Функция | | |Функция | | | Выход |
| ввода | | | поиска | | |загрузки| | |(Quite)|
|(Enter)| | |(Search)| | | (Load) | | | |
+-------+ | +--------+ | +--------+ | +-------+
| | |
+---V----+ +----V-----+ +---V---+
|Функция | | Функция | |Функция|
|удаления| |сохранения| |списка |
|(Delete)| | (Save) | |(List) |
+--------+ +----------+ +-------+
Рис. 27.1. Каждый функциональный модуль имеет только одну точку входа
Наилучший способ уменьшения вероятности появления побочных эффектов состоит в том, чтобы всегда явно передавать конкретной функции всю необходимую ей информацию. Правда, такое решение в некоторых случаях может ухудшить параметры производительности. Тем не менее, во всех случаях старайтесь избегать применения глобальных данных. Это уже правило 2, и если вы когда-нибудь писали крупные программы с помощью стандартной версии BASIC (в которой все переменные глобальные), то вы, наверное, уже понимаете важность соблюдения этого принципа.
Правило 3 устанавливает, что в тех случаях, когда все же необходимо применить глобальные данные, то сами они и те функции, которые обращаются к ним, должны быть размещены в одном файле и компилироваться отдельно от остальной части приложения. Главный принцип здесь состоит в том, чтобы объявлять глобальные данные как static, тогда они будут доступны из других файлов. К тому же, функции, осуществляющие доступ к данным типа static, могут быть сами объявлены как static, что предохранит их от вызова функциями, которые не были объявлены в том же самом файле.
Правило 4, попросту говоря, гарантирует, что программы получают «вторую попытку», так как программа, вызвавшая определенную функцию, может приемлемым образом реагировать на ситуацию, в которой возникла ошибка. Например, допустим, что при выполнении функции, осуществляющей управление закрылками самолета, непредвиденно происходит выход за диапазон представимых значений. Но вы ведь не хотите, чтобы произошел отказ всей программы (а вместе с ней и авария самолета). Скорее вы предпочтете, чтобы программа узнала, что при выполнении данной функции произошел отказ. Поскольку выход за границы диапазона может оказаться временной ситуацией для программы, обрабатывающей данные в режиме реального времени, то программа могла бы отреагировать на такую ошибку просто путем ожидания (простоя) в течение нескольких тактовых циклов, а затем попробовать повторно выполнить свою работу.
Имейте в виду, что неукоснительное соблюдение этих правил может оказаться невозможным в любой ситуации, но необходимо придерживаться этих принципов везде, где это только возможно. Подобный подход преследует цель максимизировать в создаваемой программе вероятность восстановления после сбойной ситуации, т.е. чтобы программа работала так, как если бы состояние ошибки не возникало.
На заметку | Читателям, проявляющим повышенный интерес к концепциям построения «пуленепробиваемых» функций, обязательно надо детально исследовать и поэкспериментировать с C++; он обеспечивает значительно более сильный механизм защиты, называемый инкапсуляцией, который еще больше уменьшает вероятность повреждения одной функции другой. |