Неправильное Истолкование Рассказ-предупреждение Майкл Джексон

Вид материалаРассказ

Содержание


End summary
02 Previous-record-id pic x(7) value spaces.
Value zero
End summary
End summary
End summary
Подобный материал:
Неправильное Истолкование - Рассказ-предупреждение

Майкл Джексон


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

PA: open INFILE; display 'SUMMARY'; read INFILE;

do while not eof-INFILE

if new group

end old group;

start new group

else process record

endif;

read INFILE

enddo;

display 'END SUMMARY';

close INFILE

end PA;

Теперь вы, мои эрудированные читатели, возможно сразу же можете увидеть, что то-то здесь не совеем в порядке; но наш герой был всего лишь новичком. Поэтому он пошел напролом и закодировал свое решение в COBOL, и скомпилировал его, и скомпоновал его, и запустил его на тестовых данных. То, что вышло в итоге, было немного неожиданным (для нашего героя, но никак не для нас, более опытных программистов):

SUMMARY

..V/* V..K$/K

A172632 +15

A195923 -60

Z749321 +8755

END SUMMARY

Что же, заинтересовался он, могла бы значить странная вторая строчка? Весьма быстро он понял: это был, конечно же, результат того, что старую группу заканчивали перед первой группой — только там не было старой группы перед первой группой. Но как заставить это исчезнуть? К счастью, он знал свой COBOL весьма хорошо. Первая часть странной строчки представляла собой, конечно же, идентификатор группы, и могла быть удалена установкой команды VALUE в пункте WORKING-STORAGE

02 PREVIOUS-RECORD-ID PIC X(7) VALUE SPACES.

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

02 GROUP-TOTAL PIC S9(6)

VALUE ZERO

BLANK WHEN ZERO.

После повторной компиляции он повторно запустил тест, с радостью увидел:

SUMMARY

A172632 +15

A195923 -60

A198564

A200135 -157

Z749321 +8755

END SUMMARY

Он счастливо рассматривал эту распечатку, когда случалось так, что мимо проходил его менеджер. “Что это?” – спросил менеджер, указывая на сумму для группы A198564 – “почему вместо суммы пустота?” “Это ноль”, сказал программист. “Нет”, сказал менеджер, “здесь пустота, а я хочу, чтобы сумма здесь печаталась как ноль, а не как пустота”. Наш герой, хотя и новичок, очень быстро учился уловкам профессии программиста, и он мгновенно ответил: «вы знаете, есть технические причины, почему сумму следует печатать в виде пустоты». Это был решающий момент для менеджера, он мог бы совершить катастрофическую ошибку, спросив: «Какие технические причины?» Но он был умнее, поэтому он просто сказал: «Технические причины или не технические причины, но должен печататься ноль».

Итак, это была снова стадия разработки для нашего героя. К счастью, случилось так, что он подслушал, как один из его коллег за обедом говорил о чем-то под названием “first-time switch”. Он никогда об этом раньше не слышал, но, будучи очень смышленым, он сразу же понял, что подразумевается под названием, и как подобное устройство могло ему помочь. После обеда он добавил его в свою программу.

PA: open INFILE; display 'SUMMARY'; read INFILE;

move 0 to switch1;

do while not eof-INFILE

if new group

if switch1 = 1

end old group

else move 1 to switch1

endif;

start new group

else process record

endif;

read INFILE

enddo;

display 'END SUMMARY';

close INFILE

end PA;

Когда он запустил программу снова, выходные данные выглядели просто отлично, и программу запустили в массовое производство. Она благополучно работала 6 месяцев, а потом однажды один сотрудник из пользовательский отдел пришел для того, чтобы встретиться с нашим героем. “Посмотри”, сказал сотрудник, указывая на конец недельного отчета, “здесь нет суммы для последней группы”. Ну, я думаю, что все вы, читатели, знали все это время о том, что это должно было случиться: как-никак, операции последней и первой групп были изначально спарены, и введение switch 1 удалило одну из последних групп; поэтому должна была быть группа, которая начиналась, но не заканчивалась. И действительно, такая группа была, и ей была последняя группа в файле. Конечно, данная ошибка присутствовала в каждом из 25 отчетов, которые были подготовлены с тех пор, как программа была запущена в производство, но никто не заметил этого раньше. Есть много компьютерных распечаток, которые никто не читает в большинстве установок. Программисту, конечно, не хотелось говорить ничего подобного сотруднику, поэтому он просто сказал: «Хорошо, я исправлю». Почему программист не заметил ошибку в тестировании? На самом деле потому, что он был слишком добросовестным. Он решил тщательно протестировать программу, используя все фактические данные за прошлый год в качестве тестовых данных. Это специальный вид тестирования, который называется “объемное тестирование” или “soak testing”. Это специальный вид тестирования, потому что вы выполняете программу на вводных данных, но вы не смотрите на выходные данные.

В конце концов, кто может просмотреть стопку бумаги 5 дюймов в высоту? То, что вы делаете, когда вы получаете 5 дюймов выходных данных, выглядит следующим образом: вы выкидываете первые 2 листа, потому что это язык управления заданиями, что ни один из простых смертных не понимает (эксперты иногда проверяют, чтобы код завершения системы был равен 0, но не все являются экспертами); вы проверяете, чтобы здесь не было дампа ядра; вы смотрите на первую и последнюю строчки; вы делаете выборочную проверку сумм; затем вы быстро перелистываете все 5 дюймов, подняв край стопки бумаги и позволяя листам бумаги скользить по вашему большому пальцу, для того, чтобы убедиться, если ли там что-то действительно скверное, вроде 40 последовательных страниц, на которых каждая строчка напечатана с нулями поперек страницы, тогда это само по себе обратит на себя ваше внимание. И это все. Таким образом,

ошибка осталась незамеченной. Но это было очень легко исправлено. В конце программы программист поместил команду закончить последнюю группу:

read INFILE

enddo;

end last group;

display ‘END SUMMARY’;

close INFILE

end PA;

Все было хорошо следующие 17 месяцев. И затем программа показала, что она была чувствительна к чему-то, чего вы никак не ожидали от программы: двухсотлетний юбилей. А произошло следующее: компания предоставила каждому отпуск в течении недели; в следующий понедельник они вернулись на работу и запустили программу, точно так же, как они запускали ее каждое утро понедельника. Из этого вышло следующее:

SUMMARY

$$..V/* ..D»K./

END SUMMARY

Стало сразу же понятно, что что-то не так. На предыдущей неделе не было никаких транзакций, поэтому не было никаких групп, и таинственная строчка была, конечно, результатом окончания последней группы – только не было последней группы. Но опять проблему легко решили. У нашего героя, теперь более опытного, было соблазн сделать что-то умное с switch 1, но он ему не поддался. Он уже слышал о “безопасном программировании». Безопасное программирование это теория, основанная на идее о том, что когда вы программируете, у вас нет ни малейшего представления о том, правильно или нет то, что вы делаете, поэтому вам следует делать что-то, что не причинит много среда. В соответствии с этим замечательным принципом, который, он чувствовал, был явно применим в его случае, он отказался от соблазна сделать что-то с switch 1 и вместо этого ввел switch 2:

PA; open INFILE; display 'SUMMARY'; read INFILE;

move 0 to switch1; move 0 to switch2;

do while not eof-lNFlLE

if new group

if switch1 = 1

end old group

else move 1 to switch1

endif;

start new group;

move 1 to switch2

else process record;

move 1 to switch2

endif;

read INFILE

enddo;

if switch2 = 1

end last group

endif;

display 'END SUMMARY';

close INFILE

end PA;

После повторной компиляции он запустил программу снова в пустом файле, и она выдала результат, на который он надеялся:

SUMMARY

END SUMMARY

Понятно, его треволнения закончились. Программа успешно работала следующие 19 месяцев, и не было слышно никаких жалоб. Затем, однажды, он тихо сидел в своем программистском отсеке – у Герри Вайнберга было бы что сказать по этому поводу, я думаю – читая объявления о вакансиях, когда появился сотрудник из user department. «Посмотри», сказал он, «последняя группа снова не попала в распечатку». И так оно и было. Диагноз был поставлен легко. Каким-то образом в библиотеке программы произошла путаница, и именно старая, неправильная версия программы работала на той неделе. Но оказалось, что все было не так. После пары дней детективной работы наш герой установил без сомнения, что работала именно текущая версия программы. Тем не менее, он так же установил, что программа была недавно перекомпилирована компилятором новой шестой версии, для которого инсталляция была испытательной площадкой. Должно быть, все произошло по вине компилятора! С помощью системных программистов наш герой внимательно проверил каждый шестнадцатеричный символ объектного кода и соотнес его с исходным кодом. Эта работа заняла всего лишь 9 часов, и они выполнили ее за один за один присест, заработав таким образом бонус от своего благодарного руководства. Но в результате они доказали, что объектный код был абсолютно приемлемой компиляцией исходного кода на COBOL! Оставалась только одна возможность: должно быть, это был сбой в аппаратном обеспечении или в операционной системе. Так вот, люди не любят приходить к такому выводу, если они могут этого избежать; но я вынужден сказать, что я никогда не встречал программиста, который, в частной беседе, наедине только со своими коллегами-программистами, не мог бы вспомнить по крайней мере один случай в прошлом, когда что-то смешное случилось в одной из его программ, что должно было быть объяснено этой причиной. И, как кажется, это один из таких случаев. Конечно, эта ошибка не происходила раньше – по крайней мере, после того, как команды «закончить последнюю группу» были добавлены в программу. И она не произошла снова. Тем не менее, что-то смешное действительно произошло недавно. Сотрудник user department пришел к программисту. «Я все думал», сказал он, «о той неделе, когда мы потеряли последнюю группу из распечатки. Я получил входные данные на перфокартах – который мы всегда храним несколько месяцев – и я обнаружил, что там ровно 843 перфокарт». «Ну и что?» - сказал программист.

«Видишь ли», продолжил сотрудник, «в распечатке на той неделе были 842 суммы по группам, а должно было быть 843».

«Я не вижу никакой связи», сказал программист, «но очень любезно с твоей стороны сказать об этом. Спасибо за хлопоты». Той ночью он тайно забрал распечатку программы домой и внимательно ее изучил. Внезапно до него дошло. Конечно же! Если было 843 перфокарты и 843 группы, то тогда каждая группа содержала точно одну перфокарту; таким образом, условие «new group» было справедливым для каждой перфокарты, и это всегда было тем «if», которое выполнялось, и это никогда не было «else». Но команда установить switch 2 действовала только в пункте с «else»! Таким образом, switch 2 никогда не устанавливался, и последняя группа никогда не заканчивалась. После того, как проблему выясняли, ее легко решили. В соответствии с принципами безопасного программирования, другая команда “move 1 to switch 2” была добавлена к первому условию “if - else”, оставляя первоначальную команду нетронутой в части с “else”. Таким образом, теперь программа выглядела следующим образом:

PA: open INFILE; display 'SUMMARY'; read INFILE;

move 0 to switch1; move 0 to switch2;

do while not eof-INFILE

if new group

if switch1 = 1

end old group

else move 1 to switch1

endif;

move 1 to switch2;

start new group

else process record

move 1 to switch2

endif

read INFILE

enddo;

if switch2 = 1

end last group

endif;

display 'END SUMMARY';

close INFILE

end PA;

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

Условие «новая группа» в псевдокоде программы проверяет каждую запись для того, чтобы проверить, является ли она записью смены операции или нет. Эта структура неправильная.

Ни менее предпочтительная, ни грубая, о откровенно ошибочная. И вот почему. Все сложности были вызваны командами «закончить группу». Далее, как часто мы должны заканчивать группу? Что значит как часто? Один раз на каждую группу! Где в структуре программы есть компонент, который обрабатывает каждую группу? Такого компонента нет, и команды «закончить группу» не могут быть, поэтому, не может быть правильно размещена в структуре программы. Вот в чем вся сложность. Далее, все вы, дорогие читатели, народ опытный, и вы бы не допустили таких ошибок, по крайней мере, не в такой маленькой и известной задаче, и уж тем более нет, если вы прочитали «Принципы разработки программ». Но я знаю некоторых очень опытных программистов, которые допускают такого рода ошибки в более крупных и более нечетких программах; по сути, многие из ошибок, которые они допускают, относятся именно к такому типу – неправильная структура программы. Но я не думаю, что кто-то из таких программистов сейчас читает этот рассказ-предупреждение, не так ли?