ПРОФЕССИОНАЛЬНЫЙ ПОДХОД
Мы продолжаем печать заметок известного британского программиста Стива Тернера и сегодня мы поговорим о самом щепетильном вопросе, связанном с программированием - об ошибках в программах. (Начало см. на стр. 49, 70, 117, 143)
Многие из Вас, возможно, видели отрывки из американского фильма "Звездные войны". Наиболее захватывающие сцены космических сражений в этом фильме моделировались с помощью компьютеров и, надо сказать, что суммарные трудозатраты программистов на подготовку всего обширного комплекса программного обеспечения составили 3 тысячи человеко-лет.
Спрашивается - как же можно разрабатывать такие грандиозные проекты и создавать такие обширные системы без ошибок, способных свести на нет огромные трудозатраты.
Да, конечно, судьба планеты не зависит от правильности работы Вашей программы, но в принципе те же проблемы стоят и перед Вами. Почему в программах всегда есть "жучки"? Почему программисты так много ошибаются? Можно ли избежать ошибок или, по крайней мере, постараться удалить их из готового продукта?
Ошибки делают все. Одна медсестра сказала, что если бы она делала столько ошибок, сколько делают программисты, никто из ее пациентов не выжил бы.
Я думаю, что она на самом деле тоже делает немало ошибок. Все дело в том, что они незаметны и не являются очень критическими, а к тем действиям, которые могут быть критическими, ее ведь просто не допускают - их делают дипломированные врачи. В нашей повседневной жизни мы совершаем массу действий, не являющихся критическими и поэтому не замечаем ошибок. И действительно, в любом деле между полным успехом и полным провалом есть еще огромный спектр возможных результатов, включающий в себя все виды нашей естественной человеческой неаккуратности.
Если Вы во время ходьбы споткнетесь, то Ваш мозг автоматически даст команду на корректировку - он ведет постоянный контроль за положением Вашего тела. И только когда Вы ведете работу, не осуществляя такого постоянного контроля - тогда и начинаются все неприятности. Хотя даже и в этом случае рядом может оказаться кто-то другой, кто заметит ошибку и исправит Вас.
Совсем по-другому обстоят дела в программировании, ведь компьютеру все равно правильную или неправильную команду он встречает. Он все равно старается сделать все, что может, чтобы ее исполнить и не заботится о последствиях. Мы по-человечески закладываем в программы массу естественных ошибок, а компьютер слепо все это обрабатывает.
Как правило, ошибки в программировании имеют отдаленные последствия. Время между совершением ошибки и ее проявлением измеряется часами, днями и даже годами, но суть та же. Все мы делаем ошибки постоянно. Разница в том, что ошибки программиста имеют отложенное действие, как бомба с часовым механизмом.
Можно ли перестать делать ошибки? Нет, быть совершенством невозможно. Единственное, что можно сделать - постараться свести их количество к минимуму.
Путь к уменьшению количества ошибок лежит через дисциплину программирования и самоконтроль, а дисциплина и свободное творчество находятся в постоянном конфликте.
Вопрос исключения ошибок, таким образом, сводится к самоконтролю, а самый лучший способ - это расставить ловушки на всем пути. Причем если Вы думаете, что надо исключить ошибки только в программировании, то заблуждаетесь. Их надо исключить еще раньше - уже на этапе проектирования программы. Согласитесь, что нет ничего обиднее, чем узнать, что вся Ваша концепция порочна и неверна. Конечно, мы не можем охватить все случаи, в которых могут возникнуть ошибки, но на наиболее ответственные этапы мы все-таки укажем.
Ошибки в проекте.
1. Размер программы.
Подготавливая проект, оцените размеры всех входящих в него данных: файлов, таблиц, буферов и т.п. Оценить размер программного блока - сложнее, но надо опираться на собственный предыдущий опыт и постоянно контролировать процесс. Попробуйте составить список подпрограмм, которые войдут в Вашу программу. Обязательно оцените средний размер памяти, отпущенный Вам на среднюю подпрограмму.
Когда будете программировать, то постоянно сверяйтесь - укладываетесь ли Вы в этот размер.
2. Быстродействие программы.
Часто скорость является критическим фактором, особенно это относится к играм, имеющим движущиеся объекты. Оцените время, необходимое для выполнения наиболее критических с этой точки зрения операций, например операции по перестроению изображения на экране. Прикиньте, сколько кадров в секунду Вы можете достичь, проверьте себя на простейшем тесте.
Если что-то не получается - подумайте о том, чтобы использовать другие команды процессора или другие методы адресации. Если и это не помогает - может быть можно уменьшить экранную область? Вы же ведь видели не раз, как в программах например используется для динамической графики только верхняя треть экрана, а остальное поле занято статическим, не изменяющимся рисунком.
3. Концепция программы.
Проверяйте концепцию программы на простом примере на БЕЙСИКе, можно это делать и вообще на листе бумаги. Попробуйте объяснить суть задуманной Вами программы кому-нибудь и подумайте над высказанными Вам соображениями. Представьте себя на месте будущего пользователя Вашей программы и задайте себе вопрос: "А что бы я сделал, если бы захотел сыграть в эту игру не по правилам, заложенным программистом?".
4. Цели программы.
Очень часто (особенно если это не игровая, а деловая или прикладная программа) проработка концепции еще не ведет к успеху. Надо абсолютно точно знать, чего же Вы хотите достичь. Прежде, чем начинать проработку проекта, надо составить список целей, для которых создается программа, и, по мере развития проекта, постоянно с этим списком сверяться.
5. Интерфейс "человек-компьютер".
Если Вы предполагаете использовать в программе новый для Вас метод управления, его необходимо проверить на модели. А не слишком ли он сложен? Сможет ли его освоить и применять с удовольствием средний пользователь?
6. Структура программы.
Проверьте всю структуру программы, пройдясь по ней с карандашом в руках вдоль и поперек. Расскажите ее кому-нибудь из знакомых. Объясните, как все работает и что за чем следует. Обязательно постарайтесь получить вопрос типа: "А что будет, если... ?", - и ответьте на них. Проиграйте всю программу в уме, следя за блоками структурной диаграммы. Постарайтесь в этот момент стать глупой машиной.
Наметьте для себя точки в программе, в которых устанавливаются основные переменные и в которых они изменяются.
Ошибки в программировании.
Я выписал 10 наиболее часто встречающихся ошибок в программах, написанных в машинном коде. Они расписаны по убыванию вероятности их появления. Есть прямой смысл иметь этот список перед глазами. Если что-то у Вас идет не так, 90%, что Ваша ошибка есть в этом списке.
1. Переменная или регистр процессора не установлены так, как это надо перед
началом операции.
2. Перепутаны условия перехода и т.п., например, написано JR C вместо JR NC.
3. В операциях с регистрами процессора перепутаны операнды, например: LD A,B вместо LD В,А
4. Перепутаны данные и адреса. Например LD HL,1000 вместо LD HL,(1000)
5. Неверно обрабатывается счетчик цикла. Всякий счетчик должен быть:
- установлен;
- вызван;
- изменен;
- сохранен.
6. Не рассмотрен "нулевой" вариант. Не учтено, что будет, если переменная или счетчик будут равны нулю. А если пользователь по запросу введет нуль?
7. Регистр процессора или переменная используются для какой-то второй цели, а их содержимое не сохранено. Наиболее часто это происходит, когда подпрограмма вызывает другие подпрограммы и т. д. или когда Вы вставляете внутрь своей программы новые строки.
8. Арифметические ошибки. Особенно часто возникают, когда в операции участвуют 16-битные и 8 битные числа.
9. Взаимодействие между процедурами. Должно быть строго определено что и откуда процедура принимает и что и куда она возвращает.
10. Перепутаны шестнадцатеричные и десятеричные числа.
Пути сокращения количества ошибок.
1. Сначала проектирование - программирование потом.
2. Структурируйте программу.
3. Документируйте программу (процедуры и переменные).
4. Старайтесь писать как можно меньше машинного кода. Максимально используйте свои старые процедуры, копируйте похожие куски программы.
5. Пишите в одной и той же манере. Например, организуйте циклы одним и тем же способен и изменяйте счетчик цикла в одном и том же месте.
6. Будьте проще. Пишите простой код. Не пользуетесь без особой нужды головоломными приемами.
7. Без нужды не трогайте стек.
8. Новые процедуры сначала расписывайте на бумаге.
Одним из неудобств является то, что на экране Вы можете одновременно видеть лишь очень малый кусок программы. Я считаю, что программисту, работавшему над крупным проектом, принтер необходим. После написания новой процедуры я ее распечатываю и делаю ее "прогон" в уме. Конечно, все возможные варианты ее работы отследить невозможно, но по крайней мере первый прогон по ней и последний я анализирую обязательно.
В вычислительных операциях я прикидываю минимальное и максимальное возможные значения.
В операциях сдвига и в операциях с битами приходится на бумаге рисовать карту битов в байте. Конечно это я делаю не всегда, а когда компьютер не желает исполнять свеженаписанную процедуру.
Как-то раз я написал процедуру, которая сразу заработала. Я был ошеломлен настолько, что не поверил сам себе. Два часа я разбирал ее на части в уверенности, что так не бывает. И нашел в ней такое ... - самую настоящую мину с часовым механизмом, которая только ждала своего часа.
Для тех, кто пишет на БЕЙСИКе.
Как найти ошибку в БЕЙСИК-программе? Что делать, если программа отказывается работать.
1. Начните искать ошибку в строках, которые Вы изменяли последними. Обычно
ошибка в них.
2. Постарайтесь локализовать место ошибки путем запуска не всей программы, а только ее части. Ее начало определит команда RUN n или GO TO, а конец можете обеспечить вставкой STOP.
Начинайте это делать с достаточно большого блока и уменьшайте его постепенно.
3. Если в программе есть или создаются массивы данных, то можете написать маленькую программку для проверки этих массивов. То ли в них содержится, что должно быть.
4. Оговорите суть проблемы со своим другом. Обычно, когда Вы умеете хорошо объяснить суть проблемы, то и решение уже рядом.
5. Попробуйте вжиться в проблему настолько, чтобы засыпать с нею в уме. Нередко утром Вас осеняет простое решение. Конечно это стоит делать только для особо важных и сложных проблем.
6. Распечатайте листинг проблемной области. Прокрутите работу этого блока всухую (в уме). Наметьте точки, в которых Вы вставите STOP (точки прерывания). Когда программа встанет в точке прерывания, прямой командой PRINT проверьте значения важнейших переменных.
А что делать, если программа не Ваша, если Вы набрали ее из распечатки в журнале, и она не работает?
Надо сразу сказать, что проблема эта довольно типичная. Большинство ошибок возникает при наборе, многие вызваны непропечаткой символов, а есть и такие, которые заложены уже в приведенный листинг. Для всех категорий ошибок есть масса причин, вызывающих их появление, и это конечно проблема, но решение ее - довольно полезная задача. Все программисты знают, что настоящий профессионализм приобретается не тогда, когда находишь собственные ошибки, а тогда, когда чужие.
1. Запускайте программу по частям, используя оператор STOP и проверяя переменные после остановки. Если все в порядке, запускайте программу дальше оператором CONT.
2. Найдя строки, в которых происходит ошибка, проверьте правильность своего набора. Обратите внимание на соответствие прописных и строчных букв, на похожесть буквы I и цифры 1, а также буквы B и цифры 8. Постарайтесь понять, что делает каждый оператор и проследить из каких других частей программы могла прийти ошибка вместе с какой-либо переменной. Если Вы поймете, что делают строки программы, то сможете обнаружить даже и те ошибки, которые содержатся в приведенном листинге.
3. Особое внимание обратите на ошибки, связанные с DATA и с POKE - их наиболее трудно отыскивать, часто здесь приходится не просто проверять правильно ли Вы набрали текст программы, но и разбираться с тем, что и для чего они вводят.
4. Внутри циклов используйте оператор PRINT, чтобы следить за ходом изменения параметра цикла и основных переменных.
5. Ищите улики. Пробуйте разные значения в INPUT и следите за тем, какие подпрограммы их обрабатывают. Старайтесь точно определить условия, в которых они начинают неправильно работать.
Пробный запуск.
Несмотря ни на какие предпринимаемые меры предосторожности, некоторые ошибки тем не менее все же доживают до тестового запуска процедуры. Тестирование должно быть систематический. Проводите его малыми порциями. Когда компьютер "зависнет", Вы будете знать, где собака зарыта.
Постепенно наращивайте размер программы путем "прикрепления" новых блоков к уже проверенным и надежно работающим. Если какой то блок не зависит от работы прочих -испытывайте его в первую очереди и только потом проверяйте его в составе всей программы.
Точно так же я работаю и на БЕЙСИКе, только там я запускаю группы строк, проверяя содержимое переменных.
Пользовательская проверка.
Этот этап служит не столько для отыскания ошибок, сколько для проверки соответствия ее конечным целям. Кроме того, очень часто другие пользователи делают такие вводы в программы, которых программист и не предполагал и не предвидел. И это позволяет вскрыть еще немало ошибок.
На этом этапе я провожу еще и окончательную настройку программы, чтобы достичь наилучшей "играбельности".
Последний барьер.
Когда моя игра готова к выпуску, я провожу еще одну - последнюю проверку.
Я читаю всю программу в распечатке, процедуру за процедурой. Это чтение на понимание. Я должен убедить себя в том, что понимаю, что делает каждая команда. При этом обычно вскрывается и еще какая-нибудь шальная ошибка. Далее уже слово за публикой.
План тестирования.
Чтобы правильно выполнить тестирование написанных процедур, нужна систематичность. Лучше всего для начала подготовить план.
Каждому тесту дается свой справочный номер, рядом с которым выписываемся список данных, подлежащих проверке. Очень часто эти данные - это то, что вводит пользователь при работе с программой, например команды, поступающие от джойстика.
Можно быть еще более систематичным и расписать возможные условия, в которых будет работать та или иная процедура. При этом можно использовать распечатку программы, пометив из ней все группы команд, находящиеся между условными конструкциями типа JR Z, DJNZ, JP C и т.п.
Чтобы тест был всеобъемлющим, определите условия, при которых те или иные блоки должны работать. Когда же дело дойдет до реального теста, большинство возможных тестов Вы вычеркнете из своего листинга без необходимости специально задавать условия и прогонять блоки. Останутся самые необходимые пункты, которые нельзя упустить.
Убедитесь, что каждый из намеченных к тестированию блоков реально выполняет свои функции. Это не значит, что достаточно посидеть и посмотреть, как он работает. Решите заранее для себя, в каких точках Вы будете прерывать работу процедуры и проверять то ли содержится в регистрах процессора, что там должно быть и те ли значения имеют переменные, какие положено. Конечно, не каждую группу команд стоит так проверять. Это можно оставить для наиболее головоломных. А вот конец каждой процедуры - это вполне удобное место, в котором стоит это сделать.
В следующем выпуске я приведу образец плана тестирования программы, а также приложу распечатку собственной программы MONITR (Real Time Monitor - монитор, работающий в режиме реального времени), с помощью которой очень удобно делать многочисленные проверки.
(Продолжение следует)