VIII.3.5. Н е к о т о р ы е п р и ч и н ы, о с л о ж н я ю щ и е п о и с к о ш и б о к [57] Следствие 17: некомпетентность не знает преград ни во времени, ни в пространстве. Л.Питер 1. При отладке достаточно сложных программ, использующих большой набор обрабатываемых данных, иногда трудно вообще установить факт наличия оши- бок в программе, так как программист лишь приблизительно представляет, ка- кой результат должна давать программа. Например, интегрируя систему диффе- ренциальных уравнений, мы, как правило, лишь качественно представляем кар- тину изменения искомых функций и при этом не всегда можем определить коли- чество значащих цифр полученного результата. Такое положение вещей заставляет иногда на стадии отладки прибегать к дублированию получения результата другим методом. В нашем примере это мож- но сделать, проинтегрировав систему уравнений с помощью другой программы интегрирования, и затем сравнить полученные результаты. Правда, при этом возникает вопрос: как быть в случае расхождения результатов? Ведь ошибоч- ным может оказаться не первоначальный, а как раз контрольный результат! 2. В программе, как правило, содержится не одна, а несколько ошибок. Поэтому программист наблюдает эффект не одной ошибки, а результат взаимо- действия нескольких ошибок. 3. Трудно, а иногда и просто невозможно разработать достаточно полную систему тестов, гарантирующих обнаружение всех ошибок в программе. 4. Одни и те же признаки ошибки (формы проявления ошибок) могут быть обусловлены различными причинами. 5. Некоторые ошибки не проявляются сами по себе, а лишь приводят к воз- никновению других ошибок (так называемые н а в е д е н н ы е ошибки), которые и наблюдает программист. Например, обнуляя значения элементов массива и по ошибке выйдя за его границы, можно присвоить нулевое значение некоторой переменной, при испо- льзовании которой в арифметическом выражении будет зафиксировано деление на нуль, хотя вначале этой переменной было присвоено ненулевое значение и она нигде не изменялась. 6. Некоторые ошибки нельзя выявить, разбивая программу на части и отла- живая эти части по отдельности. Ошибка возникает лишь при взаимодействии этих частей. Таким образом, стратегия "разделяй и властвуй" не всегда ока- зывается применимой. 7. Иногда внесение каких-либо изменений в программу с целью лока- лизации ошибки (например, промежуточная печать данных, трассировка и т.п.) приводит к исчезновению проявлявшихся до этого признаков ошибки или к их изменению. Получается своеобразный заколдованный круг, когда всякая попыт- ка выявить ошибку лишь маскирует ее, не давая никакой информации. Эта си- туация схожа с ситуацией, имеющей место в физике микромира: использование какого-либо прибора для наблюдения процесса полностью изменяет этот про- цесс. 8. Иногда, изменяя программу методом проб и ошибок, можно устранить ошибку, т.е. она перестает как-либо проявлять себя. При этом остается аб- солютно непонятным, в чем же она заключалась. 9. Происходит не просто нечто более странное, чем мы предполагали:странность происходящего превы- шает и то, чего мы не смели предположить. Принцип Ожидаемого по Питеру В ходе отладки программист нередко допускает просчет, необоснованно принимая некоторые предположения о возможных источниках ошибок. Например, используя стандартную библиотечную подпрограмму,он полностью уверен в ее безошибочности. Такие неверные установки, как правило, либо заводят программиста в логический тупик, либо программист впустую тратит время, пытаясь обнаружить ошибку там, где ее нет. 10. Не всегда имеет место повторяемость ошибки от запуска программы к запуску, даже если в программу и данные не вносились изменения. Наиболее часто это возникает при наличии в программе переменных, кото- рым не присвоено начальное значение.В этом случае начальное значение этой переменной случайным образом зависит от содержимого соответствующей ей ячейки памяти в момент загрузки программы. В зависимости от этого значе- ния возможно возникновение ошибочных ситуаций. 11. При разработке большого программного комплекса отдельные части про- граммы создаются разными исполнителями, что значительно затрудняет согла- сование между частями. О т л а д к а программы - это прежде всего эксперимент, а не наблюде- ние за поведением программы. Различие между этими двумя понятиями удачно и точно охарактеризовал знаменитый русский физиолог И.П.Павлов: "Наблюдение собирает то,что ему предлагает природа, опыт же берет у природы то, что хочет". И, как всякий эксперимент,отладку нужно уметь про- водить. Очень важно при этом делать правильные выводы на основании данных, полученных из эксперимента. То, насколько при этом можно ошибиться, на- глядно демонстрирует следующая шутливая история [57]. Некий школьник предложил интересную гипотезу: он утверждал, что орга- ны слуха у пауков находятся на ногах, и взялся доказать это. Положив пой- манного паука на стол, он крикнул:"Бегом!". Паук побежал. Мальчик еще раз повторил свой приказ. Паук снова побежал. Затем юный экспериментатор отор- вал пауку ноги и, снова положив его на стол, скомандовал: "Бегом!".Но на сей раз паук остался неподвижен. "Вот видите, - заявил торжествующий маль- чик, - стоило пауку оторвать ноги, как он сразу оглох. А "окончив" отладку, вспомните, что, когда известного датского скульп- тора Торвальдсена (1768 или 1770-1844) спросили мнение об одной из его скульптур, он ответил: "Я не вижу в ней недостатков, из чего заключаю,что у меня хромает воображение". VIII.4. ПРИНЦИПЫ ИСПРАВЛЕНИЯ И АНАЛИЗА ДОПУЩЕННЫХ ОШИБОК Программа, свободная от ошибок, есть абстрактное теоретическое понятие. Д.Ван Тассел Ясно, что процесс отладки складывается из двух этапов: определение мес- тонахождения ошибки и последующего ее исправления. Поговорим о принципах исправления ошибок по Майерсу [51]. ┌────────────────────────────────────────────────────────┐ 1. │ Там, где есть одна ошибка, вероятно, есть и другие. │ └────────────────────────────────────────────────────────┘ Другими словами, ошибки имеют тенденцию группироваться. При исправле- нии ошибки проверьте ее непосредственное окружение: нет ли здесь каких- нибудь подозрительных симптомов. ┌────────────────────────────────────────────────────────┐ 2. │ Находите ошибку, а не ее симптом. │ └────────────────────────────────────────────────────────┘ Другим общим недостатком является устранение симптомов ошибки, а не ее самой. Если предполагаемое изменение устраняет не все симптомы ошибки, то она не может быть полностью выявлена. ┌────────────────────────────────────────────────────────┐ 3. │ Вероятность правильного нахождения ошибки не равна 100%│ └────────────────────────────────────────────────────────┘ С этим, безусловно, соглашаются, но в процессе исправления ошибки час- то наблюдается иная реакция (например "да, в большинстве случаев это спра- ведливо, но д а н н а я корректировка столь незначительна, что она пра- вильна"). ┌─────────────────────────────────────────────────────────┐ │ Никогда нельзя предполагать, что текст, который включен │ │ в программу для исправления ошибки, правилен! │ └─────────────────────────────────────────────────────────┘ Можно утверждать, что корректировки более склонны к ошибкам, чем исход- ный текст программы. Подразумевается, что корректирующая программа должна тестироваться, возможно, даже более тщательно, чем исходная. ┌────────────────────────────────────────────────────────┐ 4. │ Вероятность правильного нахождения ошибки уменьшается │ │ с увеличением объема программы. │ └────────────────────────────────────────────────────────┘ Это утверждение формулируется по-разному. Эксперименты показали,что от- ношение числа неправильно найденных ошибок к числу первоначально выявлен- ных увеличивается для больших программ. В большой программе, рассчитанной на широкое применение, каждая шестая вновь обнаруженная ошибка может быть допущена при предшествующем внесении изменений в программу. ┌────────────────────────────────────────────────────────┐ 5. │ Остерегайтесь внесения новой ошибки при корректировке.│ └────────────────────────────────────────────────────────┘ Необходимо рассматривать не только неверные корректировки, но и те, ко- торые кажутся верными, однако имеют нежелательный побочный эффект и таким образом приводят к новым ошибкам. Другими словами, существует вероятность не только того, что ошибка будет обнаружена неверно, но и того, что ее исправление приведет к новой ошибке. Поэтому после проведения корректиров- ки должно быть выполнено повторное тестирование, позволяющее установить, не внесена ли новая ошибка. Когда кто выходит из дому, пусть поразмыслит о том, что намерен делать, а когда снова войдет в дом, пусть поразмыслит о том, что сделал. Древнегреческий мыслитель Клеобул Укажем один старый прием исправления ошибок, заключающийся в использо- вании так называемых "з а п л а т" ("patch" - "заплата", "вставка в прог- рамму"). Необходимость в "заплате" возникает, когда Вы хотите вставить последо- вательность новых операторов между двумя операторами,которые мы обозначим О1 и О2. Организация заплаты происходит при помощи оператора GOTO (тут мы отступаем от одного из основных принципов структурного программирования!): 30 О1:GOTO 1000 'Переход на операторы "заплаты" 40 О2 ··· 500 END 'Окончание основной программы. ··· 1000 ... 'Операторы "заплаты" 1010 GOTO 40 ┌──────────────────────────────────────────────────────────────────────┐ │ Никогда не оставляйте "заплаты" в о т л а ж е н н о й программе! │ └──────────────────────────────────────────────────────────────────────┘ Удалить "заплату" - это значит включить операторы заплаты в основной текст программы, расположив их там, где им надлежит находиться. Интересная невыдуманная история с"заплатой" произошла на корабле "Апол- лон", облетающем Луну. Бортовая ЭВМ выдала сигнал тревоги и отказалась выполнять дальнейшие вычисления. Космонавты быстро обнаружили,что неиспра- вен аварийный датчик, дающий неправильный отсчет. Программист на Земле на- писал текст "заплаты" для программы обработки аварийных сигналов, изменяю- щий ее так, чтобы конкретный аварийный сигнал не считался аварийным. Эта "заплата" была написана прямо в кодах, и необходимые изменения текста бор- товой программы на машинном языке были продиктованы космонавтам по радио. Программа с "заплатой" благополучно довела космонавтов до Земли. Однако учтите, что "система, состоящая из "заплаток", возникших при исправлении ошибок, редко оказывается понятнее системы, которая с самого начала не имела ошибок" (Дж.Фокс). Качество работы каждого отдельного программиста существенно повышается, если выполняется детальный анализ обнаруженных ошибок или,по крайней мере, их подмножества. Эта задача трудная и требующая больших временных затрат, поскольку она подразумевает нечто большее, чем просто поверхностную клас- сификацию, такую, как "X% ошибок являются ошибками в логике" или "Y% оши- бок встречается в операторах IF". Тщательный анализ может включать в себя рассмотрение следующих вопросов. 1. К о г д а б ы л а с д е л а н а о ш и б к а? Данный вопрос является наиболее трудным, так как ответ на него требует исследования документации. Однако это и наиболее интересный вопрос. Необ- ходимо точно определить первоначальную причину и время возникновения ошиб- ки. Такой причиной может быть, например, неясная формулировка в постанов- ке задачи или коррекция предшествующей ошибки. 2. К т о с д е л а л о ш и б к у? 3. К а к о в а п р и ч и н а о ш и б к и? Недостаточно определить, когда и кем была сделана ошибка, нужно также выяснить, п о ч е м у она произошла. Была ли она вызвана чьей-то неспосо- бностью писать ясно, непониманием отдельных конструкций языка программиро- вания, ошибкой при печатании на машинке, неверным предположением, отсутст- вием рассмотрения недопустимых входных данных? 4. К а к о ш и б к а м о г л а б ы т ь п р е д о т в р а щ е н а? Ответ на этот вопрос наиболее ценен,так как позволяет осмыслить и коли- чественно обосновать накапливаемый опыт проектирования. 5. П о ч е м у о ш и б к а не б ы л а о б н а р у ж е н а р а - н е е? 6. К а к о ш и б к а м о г л а б ы т ь о п р е д е л е н а р а н е е? Ответ на этот вопрос является другим примером полезной обратной связи. Как могут быть улучшены процессы обзора и тестирования для более раннего нахождения этого типа ошибок в будущих проектах? 7. К а к б ы л а н а й д е н а о ш и б к а? При условии, что мы рассматриваем только ошибки, которые обнаружены с помощью теста, необходимо выяснить, как был написан удачный тест. Почему этот тест был удачным? Можем ли мы что-нибудь почерпнуть из него для напи- сания других удачных тестов с целью проверки данной программы или будущих программ? Такой анализ, конечно, является сложным процессом, но его результаты могут оказаться полезными для дальнейшего улучшения работы программиста. Поэтому вызывает опасения тот факт, что подавляющее большинство программи- стов его не используют! VIII.5. ОСНОВНЫЕ ПОНЯТИЯ СТРУКТУРНОГО ПРОГРАММИРОВАНИЯ Высокое качество программ может достигаться "безошибочным" программиро- ванием ("п а с с и в н ы м и" методами) и выявлением и устранением ошибок ("а к т и в н ы м и" методами). Активные методы мы уже кратко описали. П а с с и в н ы е методы основываются на применениии методологических и организационных правил проектирования программ, а также языков програм- мирования высокого уровня. VIII.5.1. М о д у л ь н о с т ь п р о г р а м м [49] М о д у л ь н о й называют программу, составляемую из таких частей - м о д у л е й, что их можно независимо друг от друга програмировать, тран- слировать, отлаживать (проверять, исправлять). Предполагается, что модули имеют небольшие размеры, четко определенные функции и, кроме того, их свя- зи между собой максимально упрощены, в частности, предполагается,что моду- ли имеют лишь одну точку входа (в начале модуля). Разбиение программы на модули при ее написании, хотя и является весьма непростым делом,позволяет существенно облегчить в дальнейшем работу над программой на других этапах. После того как в алгоритме выявлены мало зависимые друг от друга час- ти, составление программы упрощается, так как при программировании каждой из этих частей почти не приходится заботиться об их взаимодействии с дру- гими частями, что в свою очередь способствует уменьшению количества вноси- мых ошибок. Кроме того, малая зависимость модулей позволяет при необходи- мости существенно распараллелить составление программы, поручив программи- рование программистам разного класса, причем всегда можно найти подходя- щую работу и для начинающих, и для опытных программистов. На этапе отладки независимость модулей позволяет отлаживать их в любом порядке, в частности и одновременно. Считается, что ┌───────────────────────────────────────────────────────────────────┐ │ усилия, затрачиваемые на отладку модуля, обычно пропорциональны │ │ квадрату его длины [Майерс] │ , └───────────────────────────────────────────────────────────────────┘ и поэтому при тестировании небольшие размеры модулей дают возможность по- ставить задачу о проверке всех ветвей таких модулей, что ведет к увеличе- нию достоверности тестирования. Решение такой задачи является обычно недо- стижимым по отношению ко всей программе или крупным ее блокам, когда при- ходится ограничиваться лишь проверкой работы всех линейных участков блока и условий. Разумеется, и наиболее трудная часть отладки - локализация оши- бок, проводимая для модулей, при этом значительно упрощается и ускоряется. В силу минимальности логических связей между модулями облегчается, ко- нечно, и внесение исправлений в алгоритм программы, поскольку меньше при- ходится заботиться о том, чтобы при изменении одной части программы не ис- портить работу другой ее части. Учтите, что чем более мелкими требуется получать модули, тем больше трудностей возникает при проектировании и алгоритмизации программы,но тем легче будет каждый из модулей проверять и тестировать в дальнейшем.Не сле- дует, однако, забывать и о том, что слишком большое количество мелких мо- дулей может значительно увеличить трудоемкость предстоящей комплексной (стыковочной) отладки. З а м е ч а н и е. ┌─────────────────────────────────────────────────────────────────────┐ │ Серьезной помощью в разработке программ могут стать б и б л и о т е-│ │ к и с т а н д а р т н ы х, или т и п о в ы х, модулей, заранее │ │ составленные автором или другими программистами. Применение при раз-│ │ работке ранее многократно опробованных модулей, трудность использо-│ │ вания которых сводится только к заданию правильных аргументов, зна-│ │ чительно ускоряет составление программы и облегчает ее отладку. │ └─────────────────────────────────────────────────────────────────────┘ VIII.5.2. С т р о е н и е п р о г р а м м [49] Не претендуя на полноту классификации, строение программ можно охарак- теризовать одной из следующих схем: 1. М о н о л и т н о е. Программа написана цельным куском, без выделе- ния каких-либо отдельных независимых частей; 2. М о н о л и т н о - м о д у л ь н о е.Имеется достаточно большая мо- нолитная главная часть программы, в которой производятся основные вычисле- ния, и из которой происходят последовательные обращения к модулям; 3. П о с л е д о в а т е л ь н о - м о д у л ь н о е.Центральная часть программы состоит из последовательно выполняемых модулей, которые в свою очередь обращаются к другим модулям; 4. И е р а р х и ч е с к о е. Программа состоит из модулей, связи меж- ду которыми подчиняются строгой иерархии: каждый модуль может обращаться только к модулям, которые ему непосредственно подчинены. Возврат всегда должен происходить в вызывающий модуль, даже в том случае, если в вызыва- емом модуле обнаруживается ошибка, препятствующая дальнейшим вычислениям (правда, не все языки программирования имеют средства для выполнения это- го требования); 5. И е р а р х и ч е с к и - х а о т и ч е с к о е. Иерархическая (или последовательная) подчиненность модулей нарушена дополнительными связями. 6. М о д у л ь н о - х а о т и ч е с к о е. Программа состоит из моду- лей, но связи их между собой не отвечают принципу иерархии (или последова- тельности). Последовательно-модульное и иерархическое (для более сложных программ) строение, как наиболее простые по логическим связям,являются теми образца- ми, к которым необходимо стремиться при разработке программы. Допустимыми вариантами являются иерархически-хаотическое и, может быть, монолитно-мо- дульное. Помимо модульности, другим свойством, которое содействует предупрежде- нию появления в программе ошибок, является структурированность. Обычно с т р у к т у р и р о в а н н о й называется программа, логи- ческая структура которой отвечает некоторым жестко установленным требова- ниям. Уже модульную программу можно иногда считать в определенной степени структурированной, поскольку от модульной программы требуется, например, чтобы она состояла только из модулей с одним входом. VIII.5.3. С т р у к т у р н о е п р о г р а м м и р о в а н и е Структура (от лат. "structura" - "строение, распо- ложение, порядок"),совокупность устойчивых связей объекта, обеспечивающих его целостность и тождест- венность самому себе, т.е. сохранение основных свойств при различных внешних и внутренних измене- ниях. Советский Энциклопедический Словарь Впервые основные идеи структурного программирования были высказаны Эдсгером Дейкстрой в 1965 году и позже опубликованы в его работе [55]. Ос- новная задача, которую Э.Дейкстра решал, разрабатывая идеи структурного программирования, была задача доказательства правильности программы. Его внимание было сосредоточено на вопросе, "какими должны быть структуры про- грамм, чтобы без чрезмерных усилий мы могли находить доказательство их правильности". Это особенно важно при разработке больших программных систем. Опыт при- менения методов структурного программирования при разработке ряда сложных операционных систем показывает, что правильность логической структуры системы поддается доказательству, а сама программа допускает достаточно полное тестирование. В результате, в готовой программе встречаются только тривиальные ошибки кодирования, которые легко исправляются. Очевидно, что уменьшение трудностей тестирования приводит к увеличению производительности труда программистов. Это следует из того, что на тести- рование программы тратится от трети до половины времени ее разработки. Производительность труда программиста обычно измеряется числом отлаженных операторов, которые он может написать за день. Приближенные оценки показы- вают, что применение методов структурного программирования позволяет увеличить это число в 5÷6 раз по сравнению с традиционными способами про- граммирования. Заметим, между прочим, что при структурном программировании становится излишним вычерчивание б л о к - с х е м. Блок-схема вполне структуриро- ванной программы настолько тривиально проста, что о программе можно ска- зать больше по тексту, чем по блок-схеме. Итак, структурное программирование представляет собой некоторые прин- ципы написания программ в соответствии со строгой дисциплиной и имеет целью облегчить процесс тестирования, повысить производительность труда программистов, улучшить ясность и читабельность программы, а также повы- сить ее эффективность. В настоящее время вряд ли существует достаточно простое и краткое опре- деление структурного программирования. Например, Хоор[54] определяет структурное программирование как "систе- матическое использование абстракции для управления массой деталей и спо- соб документирования, который помогает проектировать программу." Структурное программирование можно толковать как "проектирование, напи- сание и тестирование программы в соответствии с заранее определенной дис- циплиной" [54]. Х.Миллс,П.Лингер и Б.Уитт в книге [69] использовали такое определение: с т р у к т у р и з о в а н н а я программа - это программа,составлен- ная из фиксированного базового множества первичных программ. П е р в и ч н а я программа - это простая программа, не имеющая прос- тых подпрограмм, состоящих более чем из одного узла. П р о с т а я программа - это программа, которая: 1) имеет один вход и один выход, 2) для каждого узла существует путь от входа до выхода, проходящий че- рез этот узел. Суть дела здесь заключается в том, что если программное обеспечение строится только из первичных и простых программ, то логика и сам ход про- цесса ее выполнения значительно проясняются благодаря структуризации. Ис- пользование таких (готовых) структур дисциплинирует разработчика программ, что в результате приводит к появлению более понятных программ, в которых, следовательно, имеется меньшее число ошибок. Перейдем к рассмотрению теоретических оснований и методов структурного программирования. Т е о р е т и ч е с к о й о с н о в о й структурного программирова- ния принято считать принципы, изложенные в классической работе Бома и Джа- копини [40]. Эта работа в оригинале на итальянском языке была опубликова- на в 1965 г., а в английском переводе - в 1966 г. В соответствии с так называемой "структурной" теоремой, сформулирован- ной и доказанной в этой работе, всякая программа может быть построена с использованием только трех основных типов блоков [40]. 1. Ф у н к ц и о н а л ь н ы й б л о к. Ему в языках программирова- ния соответствуют операторы ввода и вывода или любой оператор (группа опе- раторов) присваивания. В виде функционального блока может быть изображена любая последовательность операторов, выполняющихся один за другим, имею- щая один вход и один выход. 2. У с л о в н а я к о н с т р у к ц и я. Этот блок включает провер- ку некоторого логического условия (P),в зависимости от которого выполняет- ся либо оператор S1, либо оператор S2. Приведем аналог условной конструк- ции на языке программирования MSX-BASIC: ┌────────────────────────────┐ │ IF P THEN S1 ELSE S2 │ . └────────────────────────────┘ 3. Б л о к о б о б щ е н н о г о ц и к л а. Этот блок обеспечивает многократное повторение выполнения оператора(ов) S, пока выполнено логиче- ское условие P. Аналог блока обобщенного цикла на языке MSX-BASIC: ┌──────────────────────────────────┐ │ n: IF P THEN S ELSE ...:GOTO n │ . └──────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ Важной особенностью всех перечисленных блоков является то, │ │ что каждый из них имеет один вход и один выход. │ └──────────────────────────────────────────────────────────────┘ Кроме того, блоки S, S1, S2,входящие в состав условной конструкции или блока обобщенного цикла, сами могут быть одним из рассмотренных типов бло- ков, поэтому возможны конструкции, содержащие "вложенные" блоки. Однако какова бы ни была степень и глубина "вложенности", важно, что любая конст- рукция в конечном итоге имеет один вход и один выход. Следовательно,любой сложный блок можно рассматривать как "черный ящик" с одним входом и одним выходом. При конструировании программы с использованием рассмотренных типов бло- ков,эти блоки образуют линейную цепочку так, что выход одного блока подсо- единяется к входу следующего. Таким образом, программа имеет линейную структуру, причем порядок следования блоков соответствует порядку, в кото- ром они выполняются. Такая структура значительно облегчает чтение и пони- мание программы, а также упрощает доказательство ее правильности. Так как линейная цепочка блоков может быть сведена к одному блоку, то любая про- грамма может в конечном итоге рассматриваться как единый функциональный блок с одним входом и одним выходом.