Заметки по структурному программированию

       

О семействах программ


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

В этом разделе я постараюсь объяснить, почему предпочитаю рассматривать программу не как изолированный объект, а как представителя семейства "сопоставимых" программ. Если обратиться к традиционной терминологии, то сопоставимыми программами можно считать либо различные программы решения одной задачи, либо сходные программы для сходных задач.

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

Разумеется, одно из обязательных свойств больших программ состоит в том, что они должны допускать последующее внесение изменений. Это объясняется тем, что часто даже логически правильные программы оказываются неподходящими для вычисления (например, не удовлетворяют одному или нескольким количественным критериям). Вторая причина может состоять в том, что, хотя программа логически правильна и даже вполне удовлетворяет исходным требованиям, она оказывается правильным решением не совсем правильно поставленной задачи; при этом приходится уточнять постановку задачи и соответственно модифицировать программу.

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




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

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

(1) доказательство правильности этих двух вариантов проводилось по возможности единообразно; (2) у этих двух вариантов была по возможности одинаковая кодировка; (3) область действия модификации легко поддавалась локализации; это условие не выполняется, если переход от одной программы к другой требует сложных изменений, затрагивающих весь текст.

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

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



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

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

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



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

В одном из предшествовавших разделов ("О надежности аппаратуры") необходимость продуманной структурной организации программы представлялась как следствие требования о доказуемости .правильности программы. В этом разделе мы установили и другую причину: структура программы должна быть организована так, чтобы допускать внесение добавлений или изменений. Составленная нами программа не только должна отражать (в своей структуре) наше понимание этого факта; нужно еще, чтобы ее структура наглядно показывала, какого типа добавления могут вноситься без затруднений. К счастью, эти два требования естественно сочетаются.

Содержание раздела