Критические точки и допущения (assertions)

Защитное программирование

Защитное программирование (defensivecoding) –это метод организации программного кода таким образом, чтобы при работе системы последствия проявления дефектов в ней не приводили к сбоям, отказам и авариям. Защитное программирование, как правило, не дает нам никакой информации о том, где находится дефект, поэтому его нельзя рассматривать как замену тестированию - эти два аспекта промышленной разработки дополняют друг друга.

Следует четко понимать разницу между обработкой ошибок и защитным программированием. При обработке ошибок локализуются и исправляются ситуации, в которых выполнение идет не так, как предполагает разработчик. Защитное программирование — это попытка учесть последствия таких проблем, которые на первый взгляд кажутся «невозможными». Подобные «невозможные» проблемы делятся на две категории.

· Пример проблемы первой категории: в модуле или классе для private функции можно гарантировать, что ей всегда будут передаваться валидные аргументы. Здесь целесообразно применить защитное программирование, чтобы обеспечить«разумное поведение» функции даже в такой ситуации, которая кажется невозможной. Но если эта же функция входит в состав общедоступной библиотеки, невозможногарантировать, что она никогда не получит некорректных данных, но можно добавить обработку ошибок на тот случай, что ей будут переданы некорректные данные. Таким образом, выбираемаястратегия — защитное программирование или явное добавление обработки ошибок — зависит от области применения конкретного ПО.

· Вторая проблема – пограничные случаи, в которых возможность или невозможность возникновения определенных условий является спорной. Рассмотрим следующий набор сценариев, которые могут сложиться в программе, если она получит невалидные данные:

1. программа принимает информацию от пользователя, который может ввести невалидные данные;

2. программа принимает данные из текстового файла, написанного человеком;

3. программа принимает данные из XML-файла;

4. программа считывает файл с бинарными данными, созданный другой программой;

5. программа считывает файл с бинарными данными, содержащий контрольную сумму для проверки наличия/отсутствия в нем повреждений;

6. программа считывает временный бинарный файл, только что созданный ею же;

7. программа считывает информацию из локальной переменной (то есть из памяти), которую она только что записала.

В какой момент мы можем быть уверены, что данные не могут оказаться невалидными?

Маловероятно, что файл с невалидными данными продолжает генерировать верную контрольную сумму (сценарий 5). Тем не менее, если данные обладают повышенной критичностью с точки зрения безопасности, необходимо учесть и вероятность того, что файл был специально подправлен для получения «верной» контрольной суммы.

Возможно, что программы будут вести себя непредсказуемо, если получат поврежденные файлы с бинарными данными(сценарии 4 - 6).

В случае аппаратной ошибки, намеренной подделки или других причин только что записанная локальная переменная может неожиданно принять некорректное значение(сценарий 7).

Итак, не всегда ясно, в каких случаях может понадобится специальный код для обработки ошибок, а в каких будет достаточно защитного программирования

Пример защитного программирования

Вариант 1 Вариант 2
size_tlen = strlen(str); for (i = 0; i<len; ++i) result += evaluate(str[i]); size_tlen = strlen(str); for (i = 0; i!=len; ++i) result += evaluate(str[i]);

Почему же условия завершения цикла всегда пишутся только по первому образцу, а не по второму?

Во-первых, последствия возникновения «невозможного» условия (i==len) пагубны и, вероятно, могут привести к неприятным последствиям, например, к бесконечному циклу или нарушению доступа к памяти. Такое «невозможное» условие вполне может возникнуть в некоторых ситуациях:

· плохое оборудование могут привести к тому, что один из битов 'i' случайным образом изменит состояние;

· другой ошибочный процесс (поток) изменяет не принадлежащий ему фрагмент памяти;

· функция 'evaluate' содержит вредоносный указатель, изменяющий значение 'i'.

 

Основной прием защитного программирования – внедрение в программный код различного рода проверок на допустимость обрабатываемых данных или допустимость состояния системы в заданный момент времени. Таким образом, подход защитного программирования можно сформулировать таким образом:

"Прежде чем делать что-то - проверьте, с корректными ли данными и в корректный ли момент времени вы начинаете это делать".

Если все данные корректны –ПО функционирует в нормальном режиме. В случае, если данные неверны, запускается специально разработанная часть ПО, предназначенная для восстановления правильности функционирования и предотвращения сбоя (либо при помощи приведения данных к корректному виду, либо при помощи извещения оператора).

В настоящее время существует два основных механизма защитного программирования – проверка допущений в критических точках и обработка исключительных ситуаций.

 

Критические точки и допущения (assertions)

В любой программе есть участки, требующие предварительных проверок перед выполнением. Пример: один массив хранит имена и фамилии людей, а второй – номера телефонов. В нормальной ситуации число элементов в массивах должно совпадать и, при поиске фамилии в первом массиве, полученный индекс может быть использован для получения номера абонента:

string_vector surname;

string_vector phone;

int index = surname.find ("Петров");

foundPhone = phone.at(index);

Однако, в случае нарушения синхронизации массивов, индекс может оказаться неверным, например, выходить за границы массива. Вообще, при использовании индекса мы делаем неявное допущение о его корректности. В большинстве современных языков программирования (C, C++, C#, Java, Eiffel) существуют средства для явного задания таких допущений.

Например, в C существует функция assert(), определенная в заголовочном файле <assert.h>. Аргументом этой функции может выступать любое булево выражение. В случае, если оно равно false, функция прерывает работу программы. Таким образом, при помощи булевых выражений могут быть описаны допущения в критических точках программы. Предыдущий пример при использовании функции assert() будет выглядеть как

#include<assert.h>

string_vector surname;

string_vector phone;

int index = surname.find("Петров");

assert((index > 0) && (index <phone.size()));

foundPhone = phone.at(index);

Часто программисты определяют свою собственную функцию assert(), например, следующим образом:

#ifdef NODEBUG

#define assert(ignore) 0

#else

#define assert(ex)    ((ex)? 1: (printf("Assertion failed "))

#endif// NODEBUG

 

Эта функция отличается от стандартной тем, что в финальной сборке системы с установленным макросом NODEBUG, выдача предупреждений функцией assert() отключается. Такая организация функции assert() связана с широко распространенным заблуждением касательно того, что частые вызовы функции assert() значительно замедляют выполнение программы. Поэтому многие программисты используют допущения только на стадии отладки, считая, что они не могут сработать в конечном продукте. Однако достаточно небольшой проигрыш в скорости окупается дополнительной гарантией надежности системы.

Другая причина того, что программисты предпочитают отключать допущения в финальной версии кода, заключается в том, что при срабатывании допущения выполнение программы прерывается. Однако, существует метод использования допущений совместно с обработкой исключений (см. следующий раздел), при котором возможно определить функции, исправляющие ошибочное состояние системы и продолжающие ее выполнение.

Целесообразность использования допущений в критических точках:

· явно задавая условия, необходимые для корректной работы программы в критических точках, мы защищаем себя от выдачи программой неверных данных;

· помощь в сопровождении документации и документировании - явно заданные условия позволяют проще определить, синхронизирован ли программный код с проектной документацией, которая задает диапазоны значенийобрабатываемых данных;

· помощь в отладке - неверные допущения проявятся в процессе отладки, в результате упростится уточнение допущений.

Выделяют следующие их допущений:

· предусловия - такие допущения помещаются в начале функций или процессов обработки данных и предназначены для проверки того, все ли необходимые данные корректны;

· постусловия - такие допущения помещаются в конце функций или процессов обработки данных и предназначены для проверки полученного результата на корректность до того, как передать его дальше;

· инварианты классов - такие допущения предназначены для периодической проверки состояния данных объектов классов, которые не должны меняться в течение жизненного цикла объекта или должны меняться строго определенным образом. Примером инварианта класс может быть требование, чтобы число элементов в списке всегда было неотрицательным.

· инварианты циклов - такие допущения предназначены для проверки условий, которые должны быть всегда истинны во время выполнения цикла. Примером такого условия может служить допущение о том, что значение итератора цикла не должно превышать количества элементов массива, по которому организован цикл.

Применение допущений является основной для контрактного программирования, при котором в каждом классе явно определяются предусловия для использования методов этого класса. Таким образом, между объектами этого класса и другими объектами заключается своего рода "контракт" в котором четко прописан интерфейс взаимодействия не только по форматам вызова методов, но и по пересылаемым данным.

Основным источником информации для определения контрактов являются требования на систему - функциональные или архитектурные. Поэтому верификацию допущений необходимо проводить в два этапа: на первом этапе проверяется корректность условий каждого допущения по отношению к требованиям, и только затем на втором этапе моделируются ситуации, приводящие к нарушению допущения. Начинающие тестировщики часто забывают о первом этапе, переходя от функционального тестирования по требованиям к структурному тестированию по коду.

Инварианты класса


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: