|
Программирование >> Разработка устойчивых систем
В приведенном примере data - динамический массив с элементами типа int и емкостью capacity, в котором используется nextSlot элементов. Функция grow() должна расширить data так, чтобы новое значение capacity было строго больше nextSlot. От этого решения, обусловленного архитектурой приложения, зависит правильность работы массива MyVector, и если прочие части программы не содержат ошибок, данное условие всегда должно выполняться. Мы организуем его проверку макросом assert(), определенным в заголовочном файле <cassert>. Макрос assertO стандартной библиотеки С лаконичен, предельно конкретен и работает на всех платформах. Если результат выражения, передаваемого в параметре, отличен от нуля, то программа продолжает выполняться. В противном случае в стандартный поток ошибок выводится текст проверяемого условия с именем исходного файла и номером строки, а программа аварийно завершается. Не слишком ли радикально? Практика показывает, что продолжение работы программы с нарушением базовых условий приводит к гораздо худшим последствиям. Программу необходимо исправлять. Если все идет нормально, к моменту выхода окончательной версии продукта ваш код будет протестирован с проверкой всех утверждений (мы поговорим о тестировании далее). В зависимости от характера приложения может оказаться, что проверка всех утверждений слишком сильно отразится на его эффективности. Но в этом случае существует простой выход: автоматически удалить код утверждений. Для этого достаточно определить макрос N DEBUG и построить приложение заново. Чтобы вы лучше поняли, как это делается, рассмотрим типичную реализацию assert(): #ifdef NDEBUG Idefine assert(cond) ((void)O) #else void assertlmpl(const char*, const char*, long): Idefine assert(cond) \ ((cond) ? (void)O : assertlmpl(???)) #endif При определенном макросе NDEBUG этот фрагмент вырождается в выражение (void)О, поэтому в потоке компиляции фактически остается лишь пустая команда - результат присутствия символа точки с запятой (;) после каждого вызова assert(). Если символическая переменная NDEBUG не определена, assert(cond) расширяется до условной команды, которая при нулевом условии cond вызывает функцию, специфическую для компилятора (assertlmpl()). Строковый аргумент этой функции представляет текст cond, имя файла и номер строки программы, в которой проверялось утверждение. (В нашем примере использован заполнитель ???, но на самом деле возвращаемая строка генерируется в точке вызова макроса. Вопрос о том, откуда берутся необходимые данные, не относится к обсуждаемой теме.) Чтобы разрешать или запрещать проверку утверждений в различных точках программы, необходимо не только включать директивы #define NDEBUG или #undef NDEBUG, но и заново включать файл <cassert>. Макросы обрабатываются по мере их обнаружения препроцессором и поэтому используют текущее состояние NDEBUG для точки включения. Чаще всего NDEBUG определяется сразу для всей программы при помощи ключа компилятора: либо в настройках проекта в визуальной среде, либо в командной строке вида туес -DNDEBUG myfile.cpp В частности, он изобрел алгоритм быстрой сортировки. С концептуальной точки зрения происходящее эквивалентно проверке утверждения, но поскольку выполнение программы не должно прерываться, макрос assert() в данном случае не подходит. Например, в Java 1.4 при нарушении утверждения запускается исключение. Большинство компиляторов использует флаг -D для определения макросов в командной строке (замените туес именем своего компилятора). Преимущество такого подхода состоит в том, что вы можете оставить утверждения в исходном тексте профаммы как бесценную документацию, и при этом полностью избавиться от лишних затрат на стадии выполнения. Поскольку код утверждения исчезает при определении NDEBUG, очень важно, чтобы в утверждениях не выполнялась никакая полезная работа - только проверка условий, не влияющих на состояние профаммы. Стоит ли использовать макрос NDEBUG в окончательной версии профаммы? На эту тему до сих пор нет единого мнения. Тони Хоар (Топу Ноаге), один из самых авторитетных программистов всех времен и народов, считает, что отключение проверок на стадии выполнения напоминает энтузиаста-яхтсмена, который носит спасательный жилет на суше, но снимает его перед выходом в море. Если вдруг окажется, что утверждение не выполняется в готовом продукте, у вас возникнут серьезные проблемы... куда более серьезные, чем небольшое снижение быстродействия. Короче, выбирайте разумно. Однако не все условия должны проверяться при помощи утверждений. Как подробно объяснялось в главе 1, о пользовательских ошибках и сбоях ресурсов лучше сообщать запуском исключений. В процессе черновой разработки профаммы возникает искушение - использовать утверждения для большинства ошибок, чтобы позднее заменить многие из них более надежной обработкой исключений. Как и при любом искушении, здесь необходима осторожность, потому что позднее вы можете забыть обо всех необходимых изменениях. Помните: утверждения предназначены для проверки условий, определенных архитектурой приложения, которые могут быть нарушены лишь из-за логических ошибок программиста. В идеале все нарушения утверждений должны быть обнаружены на стадии разработки. Не используйте утверждения для проверки условий, не находящихся под вашим полным контролем (например, зависящих от пользовательского ввода). В частности, не используйте утверждения для проверки аргументов функций; лучше запустите исключение logic error. Применение утверждений как средства проверки правильности профаммы было формализовано Бертраном Мейером (Bertrand Meyer) в предложенной им методологии проектирования по контракту . Каждая функция за1С71ючает со своими клиентами неявный контракт, в соответствии с которым выполнение некоторых предусловий гарантирует выполнение некоторых постусловий. Иначе говоря, предусловия определяют требования к использованию функции (например, передача аргументов со значениями в определенных интервалах), а постусловия определяют результаты, выдаваемые функцией (возвращаемое значение или побочные эффекты). Если клиентская профамма предоставляет недействительные входные данные, необходимо сообщить ей о нарушении контракта. Завершать профамму все же не стоит (хотя вы имеете на это полное право, поскольку контракт был нарушен), правильнее запустить исключение. Именно в таких случаях стандартная библиотека С++ генерирует исключения, производные от logic error, - такие, как out of range. Для запоминания этого принципа существует хорошая формулировка: Требовать не больше; обещать не меньше , впервые выдвинутая Маршаллом Клайном (Marshall Kline) и Грегом Ломоу (Greg Lomow). Поскольку предусловия могут ослабляться в производных классах, их иногда называют контравариантпъши, тогда как постусловия, наоборот, являются ковариттпыми (кстати, это объясняет упоминание ковариантности спецификаций исключений в главе 1). Данный раздел основан на статье Чака Эллисона ТЬе Simplest Automated Unit Test Framework That Could Possibly Work* в журнале C/C++ Users Journal*, сентябрь 2000 г. Но если речь идет о функции, которую вызываете только вы и никто другой (например, закрытая функция в спроектированном вами классе), макрос assert() оказывается более уместным. Вы в полной мере контролируете ситуацию и, несомненно, хотите отладить свою программу перед распространением окончательной версии. Нарущение постусловий свидетельствует об ощибке в программе. Утверждения уместно использовать для любых инвариантов в любой момент, включая проверку постусловия в конце функции. В частности, это относится к функциям ic/iac-сов, поддерживающим состояние объекта. Например, в приведенном выще примере класса MyVector разумный инвариант для всех открытых функций класса мог бы выглядеть так: assert(О <= nextSlot && nextSlot <= capacity): А если nextSlot является беззнаковым целым, можно так: assert(nextSlot <= capacity): Такой инвариант называется инвариантом класса, а его соблюдение вполне может обеспечиваться проверкой утверждения. Производные классы играют роль субконтрагентов по отношению к своим базовым классам, потому что они должны сохранить исходный контракт между базовым классом и его клиентами. По этой причине предусловия производных классов не должны выдвигать дополнительных требований, выходящих за рамки базового контракта, а постусловия должны выполняться, по крайней мере, в заданном объеме. С другой стороны, проверка результатов, возвращаемых клиенту, представляет собой не что иное, как тестирование, поэтому проверка постусловных утверждений в этом случае приводит лишь к дублированию усилий. Да, утверждения помогают документировать программу, но слишком многие разработчики ошибочно полагали, будто проверка постусловных утверждений способна заменить модульное тестирование. Простая система модульного тестирования Все программирование, в конечном счете, сводится к выполнению требований. Создать требования нелегко, к тому же они могут меняться. На еженедельном совещании по проекту может выясниться, что всю неделю вы работали не совсем над тем, чего от вас хотел заказчик. Человек не может сформулировать четкие требования к программе, если в его распоряжении не будет развивающегося, работающего прототипа. Вы понемногу формулируете требования, понемногу программируете и понемногу тестируете. Затем вы оцениваете результат, после чего все повторяется заново. Возможность проведения интерактивной разработки такого рода принадлежит к числу важней-
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0.002
При копировании материалов приветствуются ссылки. |