|
Одним из методов, улучшающих программы, является структурное программирование. Он служит для организации проектирования программ и процесса кодирования таким образом, чтобы предотвратить большинство логических ошибок и обнаружить те, которые допущены. Структурное программирование сосредоточивается на одном из наиболее подверженных ошибкам факторов программирования — логике программы — и включает три главные составляющие;
5-899
1. Проектирование сверху вниз.
2. Модульное программирование.
3. Структурное кодирование.
Вначале программа может иметь довольно ясную структуру, но по мере ее расширения становится очевидной необходимость
 Рис. 2.4. Пример структурированного и неструктурированного кодирования.
изменений и корректировки. Для этой цели привлекается большое количество операторов ТО и меток. Таким образом, после автономной и комплексной отладки, а также после дальнейшей эксплуатации программы логика последней становится довольно сложной. На рис. 2.4 показаны примеры обычной неструктурированной и структурированной программ. Наиболее очевидное отлйаде состоит в том, что в неструктурированном варианте используются операторы GOTO и метки. Просмотрите обе. программы и обратите внимание на сокращение числа' переходов в структурированной программе.
2.14.1. ПРОЕКТИРОВАНИЕ СВЕРХУ ВНИЗ
Проектирование программ сверху вниз подобно написанию статьи сверху вниз. Процесс написания статьи имеет иерархическую структуру и начинается с вершины иерархии, т. е. с краткого обзора. Разработку проекта обычно начинают с исследования целей и определения основных задач, ведущих к достижению этих целей. Если проект очень большой, то необходимо провести его разбиение, которое должен выполнить компетентный и квалифицированный специалист.
Вначале необходимо написать то, что вы хотите сделать, на естественном языке. Этот шаг часто многое раскрывает. Нередко вы обнаруживаете, что не в состоянии записать задачу на естет ственном языке. В таком случае не надейтесь, что вам удастся составить программу. И потом ведь намного легче переделать описание задачи на естественном языке в период разработки сцеци-фикаций, чем переписывать потом программу, которую считают уже завершенной. Таким образом, важно сформулировать задачу правильно на стадии проектирования, чтобы не исправлять ее позднее на стадиях программирования и отладки.
Сначала напишите программу на естественном языке.
Метод проектирования сверху вниз предусматривает вначале определение задачи в общих чертах, а затем постепенное уточнение структуры путем внесения более мелких деталей. Проектирование представляет собой последовательность шагов такого уточнения. На каждом шаге необходимо выявить основные функции, которые нужно выполнить, т. е. данная задача разбивается на ряд подзадач, пока эти подзадачи не станут настолько простыми, что каждой из них будет соответствовать один модуль.
Именно такой традиционный и по существу иерархический подход применяется при создании сложных структур в других областях, ^например в технике, серийном производстве, при написании статей. Затем проект системы может быть проверен и подтвержден моделированием или расчетами. Действие каждого модуля может быть описано одной фразой. Если описание модуля занимает больше одной строки — перепроектируйте его.
Далее следует описать данные, указывая их структуру и основные процессы обработки. Это описание должно включать тщательно отобранные примеры, убедительно демонстрирующие функции системы и их наиболее существенные варианты. Такие примеры будут полезны позже на стадии тестирования. При описа* нии модуля должны быть описаны его тестовые данные. Тестирование программы неизбежно, поэтому выявление требований к тестированию (слабых и критических мест) заранее, на стадии проектирования является хорошей практикой. Логическая проверка фрагментов программы должна уменьшить необходимость тестирования конечной программы. Чтобы выполнить это "ручное" тестирование, спецификации системы должны быть достаточно точными.
Разрабатывайте тестовые данные заранее. *
Основное преимущество такого метода работы — то, что он обеспечивает создание документации. Документацию всегда начинают создавать во время разработки спецификаций, но часта результаты этой начальной работы утеряны к моменту формирования окончательной документации. Лучше всего, когда большая часть документации разработана до начала программирования.. Это также способствует улучшению программы. Ведь программист должен более тщательно обдумать структуру, данные и тестирование своей программы, чтобы записать все это на бумаге.
При использовании структурного программирования правильность программы обеспечивается уже самим методом проектирования. Создание полного проекта системы по уровням, начиная с верхнего, приводит к уверенности, что система математическогб обеспечения будет удовлетворять поставленном целям и любые допущенные ошибки станут очевидными настоЛькб рано, насколько это возможно. Проектирование должно быть завершено до начала программирования. Как только приступают к программированию, создается психологический барьер, препятствующий дальнейшим улучшениям проекта. Следует сделать несколько итераций проекта прежде, чем начать программирование. В небольших проектах, где этап проектирования является менее решающим, часто программисты приступают к программированию слишком рано. Многие программы вообще не проектируются, а создаются сразу в форме кода. 4
Прежде Чем начать программировать, разработайте проект.
Теперь, когда разъяснен метод проектирования сверху вниз, остановимся и попытаемся разработать проект, заключающийся в том, чтобы одеть мужчину. Начнем с определения задачи в общих чертах, а затем постепенно уточним инструкции, пока не получим полный набор инструкций.
Итак, попытаемся сделать это. Очевидный первый шаг мог бы установить цель:
Одеть
Первый уровень уточнения мог бы выглядеть так:
Одеть нижнюю половину Одеть верхнюю "половину
Нижнюю половину можно было бы одеть в два этапа:
Надеть трусы и брюки ~ Надеть ботинки и носки
В два этапа можно одеть верхнюю половину:
Надеть майку Надеть рубашку
Окончательный проект выглядит так:
Надеть трусы Надеть брюки Надеть ботинки Надеть носки Надеть майку Надеть рубашку
Таким образом, мы имеем довольно подробные спецификации проекта. Следующий шаг заключается в том, чтобы вручную промоделировать его и проверить ошибки. А ошибка здесь есть! Если вы не можете найти ошибку ручным просчетом, попытайтесь использовать спецификации проекта, чтобы одеться завтра утром.
Большинство современных методов создания математического обеспечения допускает появление ошибок в спецификациях проекта, которые сделаны на предварительных этапах и остаются необнаруженными до тестирования и эксплуатации. Однако цена обнаружения и корректирования ошибок в системе возрастает по мере приближения программы к завершению. Исправление ошибок, найденных во время проектирования, относительно недорого (переделать проект) по сравнению с обнаружением и исправлением ошибок на конечном этапе тестирования (перепрограммировать задачу). Кроме того, поздно найденные ошибки затрагивают многих и вызывают необходимость связи между группами (например, между программистами, теми, кто делает документацию, и заказчиками).
Исключайте ошибки с самого начала.
В большинстве случаев ошибки ,в системе — результат неполных или противоречивых спецификаций проекта. В работе [6] показано, что на стадии спецификаций проекта вносится до двух третей ошибок. Следовательно, именно здесь должны быть сосредоточены наши главные усилия, так как ошибки при проектировании дороже всего обходятся. Хорошее программирование не спасет плохой проект.
Плохо спроектированные программы настолько распространены, что введен новый термин для их обозначения: "клудж"1)— система, представляющая собой "набор плохо совместимых друг с другом частей, формирующих жалкое целое". Этот термин опре--делен в статье Дж. Грэнхолма2> и применяется сейчас для обозначения плохо спроектированной программы, неважно составленной .документации или даже неудовлетворительной работы целого вычислительного центра.
2.14.2. МОДУЛЬНОЕ ПРОГРАММИРОВАНИЕ
Чтобы преуспеть в структурном программировании, программу следует представить в виде модулей. Модульное программирование— это процес разделения программы на логические части, называемые модулями, и последовательное программирование каждой части. Когда большая единая задача делится на подзадачи, то значительно проще понять и прочесть программу. Это обычный способ управления сложной ситуацией: "Разделяй и властвуй!" Если проведено проектирование всей задачи сверху вниз, то она, естественно, разбивается на подзадачи для возможных модулей. При этом преследуют 'две цели:
1. Необходимо добиться того, чтобы программный модуль был травильным и не зависел от контекста, в котором он будет использоваться.
2. Следует стремиться к тому, чтобы из модулей можно было формировать большие программы без каких-либо предварительных знаний о внутренней работе модуля.
Прикладные подпрограммы и стандартные процедуры — примеры удачных модулей. Мы используем процедуру вычисления функции квадратного корня в любом контексте и не ожидаем побочных эффектов в других частях программы. Нам хотелось бы, чтобы наши собственные модули взаимодействовали таким же образом, не оказывая непредусмотренного влияния друг на друга.
Размер модулей
Никто не может точно назвать наилучший размер модуля, но обычно используемое ограничение — около 60 строк. Такая длина программы удобна для восприятия, так как она легко охватывается и запоминается. Это число строк помещается на одной- странице, кроме того, его легко прочесть на терминале вычислительной машины. Очевидно, существуют исключения, но тем не менее 60 строк — это удобный размер модуля.
Иногда делают модули больших размеров и руководствуются другими соображениями при выборе размера модуля. Мы хотим, чтобы модуль соответствовал одной легко охватываемой задаче. В некоторых организациях допускаются модули до 100, 200 или даже 300 строк. Нам кажется, что верхний предел 300 строк редко используется и необходимость в нем сомнительна. При выполнении нескольких больших проектов по программированию (работы [3, 4]) придерживались предела в 60 строк и получили вполне работоспособные системы. Если вы не можете ограничить подзадачу 60 строками, но справитесь с составлением более чем одной подзадачи, тогда вам следует разбить подзадачу на несколько модулей. Хотя мы не можем установить точный максимальный размер модуля, существует общее соглашение, что малый модуль лучше большого.
Короткие модули предпочтительнее длинных.
В некоторых системах размер модуля ограничивается 4096 байт, или 1024 словами. Но это кажется менее удобным, чем использование предела в 60 строк. Ведь нетрудно установить, где оканчиваются 60 строк, но не ясно, где кончаются 4096 байт.
Независимость
Следует стремиться к независимости между модулями, или подпрограммами. Для достижения этого требуется, чтобы модуль не зависел от а) источника входных данных, б) места назначения выходных данных и в) от предыстории, иначе сложность системы резко возрастает. При разбиении программы на функциональные блоки, называемые модулями, можно обеспечить некоторую самостоятельность последних, хотя определенная зависимость все-таки существует, например от параметра и последовательности вызова.
Каждый модуль должен иметь свое назначение, отличающееся от назначения других модулей. Это должен быть замкнутый блок, вход и выход которого точно определены. Стремление к функциональной независимости подпрограмм хорошо аргументировано: в этом случае менее вероятно, что изменение в одной подпрограмме воздействует на остальную часть программы. Воздействие изменения в одном модуле (или части программы) на другую часть программы называется волновым эффектом (ripple effect).- Этот эффект можно уменьшить, сведя к минимуму связь между моду-
-лями, т. е. сократив количество путей, вдоль которых изменения 7или ошибки могут проникнуть в другие части системы. Простой путь уменьшения волнового эффекта — избегать использования глобальных переменных (COMMON-переменных) и делать модули небольшими. Минимизация взаимосвязей между модулями — модульное сцепление (module coupling) — происходит за счет усиления связей между элементами одного модуля (модульная прочность — module strength). Таким образом, тесно связанные элементы мы стремимся поместить в один модуль, а несвязанные— в разные. Высокая степень независимости, характерная для структурного программирования, и минимальная связь модулей приводят к ограничению влияния дефектов средств программного обеспечения на модуль.
Использование модулей приводит к уменьшению сложности. Фактор сложности при этом включает три составляющие: функциональную, распределенную и связи. Функциональная сложность обусловлена тем, что один модуль выполняет слишком много функций. В этом случае для их различения требуется очень сложное тестирование. Распределенная сложность — это сложность идентификации общей функции, распределенной между несколькими модулями, за счет чего утрачивается возможность уменьшения сложности всей программы при модульном программировании. Сложность связи определяется сложностью взаимодействия модулей при использовании общих данных.
Наложение ограничений на размер подпрограмм не гарантирует использования метода модульного программирования при написании программ. Некоторые программисты, наслышанные про модульность, пишут программу, делят ее на четыре-шесть частей и говорят, что они используют принцип модульности. Излишне говорить, что это неправильный подход. Следует делать естественное разбиение программы на модули — разбиение, соответствующее полученному на стадии проектирования сверху вниз.
Разработка любой достаточно сложной программы состоит из нескольких различных этапов и начинается с исследования общих целей проекта. Это приводит к определению основных задач, предусматривающих осуществление этих целей. Определение основных задач — обычно первая фаза проектирования программы, в течение которой и должно выявиться разделение на модули.
Чтобы оценить независимость модулей, мы должны принять во внимание следующие обстоятельства. Если что-то в программе изменить, как это отразится на остальной части программы? Если любой программный модуль можно заменить новым, воспринимающим те же входные данные и выдающим такие же выходные, а на программе это не отразится, это означает, что модули независимы. Если существуют какие-то величины, подверженные изменениям (например, налоговые удержания в программе начисления зарплаты), то их следует выделить в отдельный модуль, чтобы облегчить внесение изменений. Небольшие усилия по планированию будущих изменений позже, во время эксплуатации программы могут "обернуться прибылью".
Определение модуля
Хорошие модули должны быть подобны математическим функциям. В математике имеется функция квадратного корня. Мы называем ее
5(ЖТ (X)
Функция квадратного корня может быть вычислена только для неотрицательных чисел. Таким образом, неотрицательные числа являются областью определения этой функции. Область определения—это набор возможных входных значений. Функция не определена для значений, выходящих за пределы этой области. Каждому допустимому аргументу X функция ставит в соответствие определенное значение. Набор возможных выходных значений называется областью значений.
Хотелось бы, чтобы модули были определены таким же образом. Модуль должен также проверять аргументы на их принадлежность области определения, как это делается для функции квадратного рорня. Когда аргумент функции квадратного корня получает значение, выходящее за пределы области определения, обычно выдается сообщение об ошибке. Это называется побочным эффектом. Иногда побочный эффект включает код ошибки, который может быть обработан. Таким образом, для модуля должны быть определены
1) алгоритм решения задачи;
2) область допустимых входных значений;
3) область возможных выходных значений;
4) возможные побочные эффекты.
Если эти четыре объекта надлежащим образом определены для каждого модуля, то результатом будут четко составленные модули, а следовательно, и хорошие программы. Однако часто область определения не проверяют, а побочные эффекты не определяют.
Следует сделать одно дополнительное замечание по поводу побочных эффектов. Лишь в редких случаях получение побочных эффектов должно прекращать выполнение программы. Вместо этого должна выявляться ошибка и должен происходить возврат к вызывающему модулю. Таким образом, нежелательно, чтобы процедура вычисления функции квадратного корня прекращала* вычисления при получении значения, выходящего за пределы об-, ласти определения (т. е. отрицательного значения). В этом случае должно быть возвращено некоторое значение (часто нуль) вызывающему модулю, предупреждающее об ошибке. Затем вызывающий модуль должен решить, прекращать или продолжать вычисления. Это гарантирует универсальность программы, так как ошибка идентифицируется и вызывающая программа может делать то, что необходимо.
Именно из-за побочных эффектов затрудняется использование программы. Побочный эффект можно определить следующим образом: если модуль Мх не может быть заменен модулем Mz, где Мх и Mz — модули с одинаковыми входом и выходом, это значит, что модуль Мх имеет побочные эффекты. Приведем пример модуля с побочными эффектами:
IF (MAP (I)..LT. 0) STOP
FUNCTION MAP (N) COMMON I, K, L
1 = 0
K = K+1 Lc=K*L
MAP = MAP — К RETURN END
He вызывающий на первый взгляд подозрений оператор IF обусловливает ряд побочных эффектов, что можно обнаружить, если тщательно изучить программу. Данный пример также показывает, почему пропадает желание использовать глобальные переменные, или COMMON-переменные. При использовании глобальных переменных побочные эффекты могут легко остаться незамеченными.
Метод кодирования
Сегменты программ должны быть составлены следующим образом. Каждый сегмент должен иметь один вход в начале и один выход в конце. Если другие сегменты вызываются внутри какого-либо сегмента, то в них входят через начало, выходят через конец и возвращаются обратно в вызывающий сегмент.
Главная процедура должна принимать все решения по управлению потоком данных для соответствующих обрабатывающих процедур. Переменные, общие для всех модулей, должны быть определены как часть главной процедуры в области COMMON. Локальные переменные должны использоваться только своими модулями.
Каждый сегмент должен иметь как можно меньше ветвей вычислений. Чем меньше размер модуля, тем, как правило, меньше ветвей, которые надо тестировать. Чем проще структура, тем лучше. Каждая процедура должна выполнять только одну логическую задачу. Следует обеспечить невмешательство других сегментов в "кухню" вычислений каждой процедуры. Таким образом, o6pa6ia-тывающая процедура будет управляться только своим сегментом. Ни одно решение, принятое за пределами сегмента, не дйлжно определять какое-либо действие внутри сегмента, и, наоборот, ни одно, решение внутри сегмента не должно сказываться на действиях вне сегмента. Четкость программы в целом в большой степени зависит от четкости структуры каждого модуля.
Таким образом, каждая процедура является замкнутой. Управление передается от главной процедуры к вызываемой, и; когда последняя выполнит свою функцию, она возвратит управление вызвавшей ее главной процедуре. Вызываемая процедура может в свою очередь вызвать другую процедуру, однако возвраг всегда будет происходить в процедуру, являющуюся вызывающее для данной процедуры.
Задача об изображении шахматной доски .
Задача: напечатайте изображение шахматной доски; ее образец приведен на рис. 2.5. Эта задача может быть использована для демонстрации метода проектирования сверху вниз и принципа модульности. Попробуйте сделать ее самостоятельно. Вероятно, существует много хороших решений, и ваше решение может оказаться даже лучше моего. Кроме того, это единственный способ выяснить, усвоили ли вы методы, описанные в данной главе.
Проведите проектирование этой задачи сверху вниз, разбив ее на модули. Потом посмотрите предлагаемое здесь, решение. Если интерес у вас не пропадет, составьте программу для этой задачи и доведите ее до конца.
Первый шаг при проектировании сверху вниз — описание того; что мы хотим делать. Задача заключается в том, чтобы напечатать изображение шахматной доски. Наша программа могла быть записана следующим образом:
CALL PRINT-CHESSBOARD
Так как не существует команды, которая бы делала это, мы долж> ны разделить нашу задачу на подзадачи. На следующем шаге* произведем деление на верхнюю полосу (ТОР), середину^ (MIDDLE) и нижнюю полосу (BOTTOM):
CALL TOP-MARGIN CALL MIDDLE
CALL BOTTOM-MARGIN
 Рис. 2.5. Шахматная доска.
Далее"" в подзадаче MIDDLE выделим две составные подзадачи, так как существуют два типа рядов шахматной доски. Попробуем это сделать:
CALL FIRST-ROW-TYPE CALL SECOND-ROW-TYPE
В этой записи FIRST-ROW-TYPE — это модуль, формирующий ряд первого типа, a SECOND-ROW-TYPE — соответственно второго типа. И если мы повторим вызовы этих модулей по четыре раза, то получим все восемь рядов шахматной доски:
DO 4 TIMES
CALL FIRST-ROW-TYPE
CALL SECOND-ROW-TYPE DOEND
Теперь рассмотрим задачу FIRST-ROW-TYPE. Нам нужно напечатать строку определенного типа шесть раз:
FIRST-ROW-TYPE DO 6 TIMES
PRINT FIRST-ROW DOEND
Аналогично поступим с рядами второго типа. Теперь мы имеем достаточно хороший проект всей задачи, разработанный с помощью метода сверху вниз и разбиения на модули. Вы, видимо, заметили, что верхняя и нижняя горизонтальные полосы модели доски одинаковы. Итак, предлагаем один из вариантов программы на некотором метаязыке:
CHESSBOARD CALL MARGIN DO 4 TIMES
CALL FIRST-ROW-TYPE
CALL SECOND-ROW-TYPE DOEND
CALL MARGIN
Заметьте, что пока мы не рассматриваем вопрос о программировании этих процедур.
2.14.3. СТРУКТУРНОЕ КОДИРОВАНИЕ
Человек имеет склонность к последовательным процессам. Мы предпочитаем последовательные чтение и обработку. Поэтому, чем больше разрывов в последовательности, тем труднее нам прослеживать логику программы. После нескольких условных переходов мы может запутаться в программе со сложным ветвлением.
Структурное кодирование — это метод написания . хорошо структурированных программ, который позволяет получать программы, более удобные для тестирования, модификации и использования. Программы произвольных размера и сложности могут 'быть написаны на основе ограниченного множества базисных структур.
Этот принцип положен в основу проектирования схем, где любая логическая структура может быть создана из элементарных структур И, ИЛИ и НЕ; в булевой алгебре имеется соответствующая теорема. Так как отмеченное положение обосновано теоретически, нет необходимости доказывать его в каждом отдельном случае. Задача по проектированию логической схемы из базисных составляющих относится к полю деятельности инженера. Если он не в состоянии сделать это, подлежит сомнению его профессиональная компетенция.
Теория структурного кодирования
Структурное кодирование состоит в получении правильной программы из некоторых простых логических структур. Оно базируется на строго доказанной теореме о структурировании (разд. "Проекты" в конце главы), которая утверждает, что любую правильную программу (с одним входом и одним, выходом, без зацикли-
 Рис. 2.6. Основные логические структуры (а — последовательности, б — выбора и в — повторения).
ваний и недостижимых команд) можно написать с использованием только следующих логических структур:
1) последовательности двух или более операторов
(MOVE, ADD, ...)
2) выбора одного из двух операторов
(IF THEN ELSE)
3) повторения (или управления циклом) оператора, пока выполняется некоторое условие
(DO WHILE)
На рис. 2.6 показаны эти основные логические структуры. Комбинации правильных программ, полученные с использованием трех основных управляющих структур (последовательности, выбора и повторения), также являются правильными программами. Заметьте, что каждая структура имеет один вход и один выход. Можно получить программу любого размера и сложности, применяя итерацию и вложение этих основных структур. При использовании только указанных трех структур отпадает необходимость в безусловных переходах и метках. Каждая структура прослеживается от начала до конца без каких-либо ветвлений. Применение структурного программирования в значительной степени уменьшает сложность программ.
 Рис. 2.7. Структура последовательности.
Структура последовательности — формализация того, что операторы программы выполняются в порядке их появления в программе, пока что-то не изменит эту последовательность. Тот факт, что последовательность является управляющей логической структурой, иногда упускается из виду. Структура выбора — это выбор одного из двух действий, исходя из выполнения некоторого условия; ей соответствует оператор IF THEN ELSE. Структура повторения используется для повторного выполнения группы команд до тех пор, пока выполняется некоторое условие. Итеративная управляющая структура приводится в действие оператором DO WHILE или итеративным DO.
Существенным моментом является то, что при замене любого функционального прямоугольника в схеме правильной программы на любую из трех основных структур программа остается в классе правильных программ. На рис. 2.7 приведена схема программы, состоящей из. последовательности трех функций. Если мы хотим заменить функцию В и использовать структуру выбора, то получим схему, изображенную на рис. 2.8. Получение правильной программы путем замены операторов программы на управляющие логические структуры называется вложением структур.
Хотя теоретически возможно написать все правильные программы, используя только три перечисленные выше основные логические структуры, мы увидим, что небольшое расширение этого базиса облегчит программирование. Введенные структуры должны иметь один вход и один выход. Обычно добавляют операторы DO UNTIL и CASE. На рис. 2.9 показаны эти две логические структуры.
Оператор DO UNTIL организует цикл, подобный циклу оператора DO WHILE. Но существуют различия между этими операторами.
1. Цикл DO WHILE оканчивается, когда условие ложно, а цикл DO UNTIL — когда условие истинно.
2. В цикле DO UNTIL условие проверяется после выполнения операторов цикла, так что операторы тела цикла выполнятся по крайней мере один раз.
Структура CASE — это схема разветвления и соединения ветвей, которая используется для выбора одной из ветвей вычисле-
 Рис. 2.8. Структура с вложением.
 Рис. 2.9. Операторы DO UNTIL и CASE.
ния в зависимости от значения целого выражения. Оператор CASE имеется только в АЛГОЛе.
Несколько уже рассмотренных ранее понятий необходимо для поддержания структурного программирования. Во-первых, логическая структура должна указываться отступами так, чтобы логические отношения в программе соответствовали позициям в листинге. Далее, программа должна быть разделена на группы команд (модули), которые легко охватываются. И наконец, программа должна быть организована таким образом, чтобы ее можно было читать от начала до конца без прерываний, обусловленных переходами.
Руководствуясь этими принципами при написании программ" можно существенно уменьшить их сложность, так как программы можно будет читать сверху вниз как печатный текст. Перескакиваний, типичных для программ с операторами GO ТО, здесь небудет. И действительно, при правильном использовании основных: логических структур почти отпадает необходимость в использовании операторов GO ТО. Следует отметить еще одну характерную черту: структурированные программы требуют более детального проектирования до программирования. Иначе невозможно остаться в пределах требуемой структуры, и мы должны прибегать к: переходам, чтобы реализовать непредусмотренные случаи.
Чтобы получить простую структуру для каждого сегмента программы, следует избегать операторов GO ТО. Каждый сегмент-должен состоять из прямой последовательности операторов IP THEN ELSE, циклов операторов CASE или таблиц решений. Такие языки, как ФОРТРАН или КОБОЛ, не имеют достаточного количества языковых конструкций для структурного программирования, поэтому при работе с ними трудно обойтись без операторов; GO ТО.
Для случаев программирования на языке, в котором нельзя-исключить операторы GO ТО, и для тех, кто не может отказаться от использования GO ТО в силу многолетней привычки, предлагаем два совета, которые помогут ограничить использование этого* оператора:
1. Не прибегайте к GO ТО там, где можно заменить его операторами CALL или PERFORM.
2. Применяйте GO ТО только для переходов вперед. Переход: назад подразумевает цикл. Используйте в этом случае соответствующую конструкцию цикла.
Как правило, следует избегать операторов GO ТО, кроме особых случаев, таких, как моделирование управляющей логической* структуры в языке программирования, в котором таковая отсутствует.
Качество программы часто обратно пропорционально числу включенных в нее операторов GO ТО. Эти операторы затрудняют чтение больших программ. В программе, написанной с помощью операторов GO ТО, читатель, просмотрев несколько строк, перескакивает через определенное число строк или страниц, чтобы прочесть еще несколько строк, опять перейти на другую страницу и т. д. Усложняя ситуацию, многие переходы являются или условными, или разветвлениями, так что мы часто сомневаемся в правильности выбранной ветви или забываем исходную задачу. Цель структурного программирования — обеспечить возможность чтения программы от начала до конца, следуя ее логике.
Использование конструкций структурного кодирования
В языках ПЛ/1 и АЛГОЛ имеется большая часть рекомендованных выше операторов, поэтому на этих языках легко писать структурированные программы. Кроме того, оба языка имеют блочную структуру, так что нетрудно разбить программу на блоки, поскольку языки позволяют это. Так как ни в одном из основных языков программирования нет всех необходимых для структурного программирования операторов, рассмотрим, как смоделировать их в любом языке.
ПЛ/1
Язык ПЛ/1 располагает такими структурами, как последовательность, IF THEN ELSE, итеративный DO и DO WHILE. Этот язык обладает также возможностью группировки, так что в программе могут быть выделены блоки, а процедуры или подпрограммы можно активировать с помощью операторов CALL или % INCLUDE. Оператор DO UNTIL не входит в состав набора операторов стандартного ПЛ/1, но его можно легко смоделировать следующим образом:
/* DO UNTIL */
MOREJTOJX)=='l'B;
DO WHILE (MORE.TO.DO);
IF (условие) THEN
MOREJTO_DO= 'O'B;
END;
Переменная MORE-TCLDO должна быть объявлена как BIT(l). Вышеприведенная модель оператора DO UNTIL всегда будет выполнена по крайней мере один раз, так как проверочное условие изменяется только в конце группы DO WHILE.
Оператор CASE также не входит в состав стандартного ПЛ/1. Вот как можно представить этот оператор:
/* CASE */
CASE JNDEX = выражение;
IF (CASE INDEX < 11CASEJNDEX > С ASE.max)
THEN CASEJNDEX =n; GO TO CASE (CASEJNDEX);
CASE1: DO;
GO TO ENDXASE; END; CASE2:
DO;
GO ТО END.CASE; END; CASE3:
; CASEn: DO;
• Это соответствует выбору ошибочного случая GO ТО END.CASE; END;
END.CASE: ; .....
При таком представлении оператора CASE требуется следующее описание:
DECLARE CASE (l:n)
LABEL
INITIAL (CASE1, CASE2, CASE3, .... CaSEn);
Этот пример подходит для любого числа разветвлений. Нужно лишь проверить, не превышает ли значение переменной CASE-INDEX допустимого значения. Оператор CASE особенно удобен, когда предстоит выбор из большого числа вариантов. Предположим, нужно выбрать одну из 50 процедур, используя двузначные целые числа. Можно, конечно, написать 50 операторов IF, но здравый смысл подсказывает, что оператор CASE сделал бы это намного лучше. В случае пустого тела CASE можно оставить только оператор GO ТО. Выбор одной и той же процедуры в нескольких случаях можно было бы осуществить, поставив более одной метки перед одним CASE.
Если возможностей для выбора немного или нет подходящего численного кода, то для моделирования структуры CASE можно использовать вложенные операторы IF:
IF (CODE = 'А') THEN
CALL CASE_A.PROC; ELSE
IF (CODE = 'C')
THEN
CALL CASE.C-PROC;
.ELSE
JF (C0DE='3') THEN
CALL CASE.3.PR0C; ELSE
CALL CODE.ERROR.PROC;
Программа может Гбыть сделана более эффективной, если упорядочить варианты с учетом наибольшей вероятности выбора.
АЛГОЛ
Большинство версий АЛГОЛа имеет все или почти все рекомендованные структуры. Обычно включается и оператор CASE. Если оператора .DO UNTIL нет, его можно смоделировать так же, жак в ПЛ/1.
КОБОЛ
В КОБОЛе структурное кодирование не встречает трудностей. КОБОЛ имеет структуры последовательности, IF THEN .ELSE, а также и оператор CALL — для подпрограмм. Предусмотрен также оператор PERFORM, который может быть использован :как итеративный DO. КОБОЛ располагает возможностью группировки операторов, но при этом ни один оператор, кроме последнего, не оканчивается точкой. Это ограниченная блочная структура, так как здесь не допускается вложенных блоков и требуется, что--бы ни один оператор в группе не оканчивался точкой.
Когда IF THEN ELSE используется для условного выполнения "единичных операторов, то не возникает никаких проблем. Структуру IF THEN ELSE можно применять также для выполнения трупп операторов, если внутри этих групп нет точки:
W (условие) THEN группа операторов 1 ELSE группа операторов 2.
Трудности возникают, когда внутри первой группы операторов -требуется IF THEN (без ELSE):
IF (условие р) THEN
IF (условие г) THEN группа операторов 1
ELSE группа операторов 2.
Сложности связаны с тем, что оператор ELSE будет спарен не с тем THEN, с которым нужно. В КОБОЛе ELSE должен соответствовать верхнему THEN, т. е. в рассматриваемом примере необходимо спарить ELSE с первым THEN. Но тогда в группе операторов, расположенной до ELSE, будет сделана синтаксическая ошибка. Эту трудность можно разрешить, если добавить еще один оператор ELSE и сочетание NEXT SENTENCE:
IF (условие р) THEN IF (условие г) THEN группа операторов 1 ELSE
NEXT SENTENCE .
ELSE группа операторов 2.
При использовании вложенных операторов IF добавление ELSE NEXT SENTENCE — наилучший способ избежать неправильного соответствия THEN и ELSE.
Оператор PERFORM вместе с оператором КОБОЛа UNTIL действует так же, как традиционный оператор DO WHILE, поскольку условие проверяется до выполнения параграфа. Это выглядит, так:
PERFORM параграф UNTIL (условие).
Моделируя оператор DO UNTIL, мы хотим, чтобы параграф вычислялся по крайней мере 1 раз. Мы можем сделать это так:
PERFORM параграф. PERFORM параграф UNTIL (условие).
Оператор CASE можно получить, применив оператор КОБОЛа GO ТО...DEPENDING:
GO ТО case-1, case-2, case-n
DEPENDING ON IDENTIFIER.
Каждый именованный параграф должен оканчиваться оператором GO ТО, ведущим к общей собирающей точке. Только один параграф будет переходить к вышеописанному GO ТО —тот, который обрабатывает значение п, выходящее за пределы запланированной области. Оператор CASE подобен описанному в разделе
ФОРТРАН
На ФОРТРАНе труднее написать структурированную програм*-му, так как в этом языке нет группирующих структур типа IF THEN ELSE. Существует несколько способов решения этой проб* лемы. ФОРТРАН имеет оператор IF, таким образом, часть проблемы уже решена. В случае единичных операторов лучше всего применить последовательно два оператора IF:
IF (условие) оператор 1
IF (.NOT. условие) оператор 2
Первый оператор IF обрабатывает часть THEN, второй оператор IF —часть ELSE. В ФОРТРАНе нет блоков. Чтобы выполнить блок операторов, определяемый оператором IF, мы можем сделать так:
С IF THEN ELSE.
IF (условие) GO TO 50 •
выполняется, если условие ложно
GO ТО 75 50 •••
выполняется, если условие истинно
75 CONTINUE С END IF THEN ELSE. Если часть ELSE не нужна, то этот фрагмент можно сократить:
С IF THEN.
IF (NOT условие) GO TO 75
часть THEN
75 CONTINUE С END IF THEN.
Это, конечно, менее наглядно, чем при наличии оператора IF THEN ELSE, но делает то же самое. Здесь соблюдается правило использования GO ТО в структурном программировании (т. е. GO ТО следует применять только для перехода вперед). ФОРТ-РАН обладает свойством модульности, которое можно использовать для формирования блоков программы.
Оператор DO WHILE в ФОРТРАНе можно реализовать несколькими способами. Во-первых, есть оператор цикла DO, которого часто бывает достаточно. Простой путь моделирования DO*
WHILE — использование стандартного цикла ФОРТРАНа с очень большим конечным значением параметра:
С DO WHILE.
DO 100 К= 1, ЮОООО IF (условие) GO ТО 150
100 CONTINUE 150 CONTINUE С END DO WHILE.
При моделировании оператора DO UNTIL в конце тела цикла делается проверка на невыполнение условия:
С DO UNTIL.
DO 100 К—1, ЮОООО
IF (NOT условие) GO TO 150 100 CONTINUE 150 CONTINUE С END DO UNTIL.
И наконец, можно прибегнуть к конструкции CASE, которая реализуется в ФОРТРАНе с использованием вычисляемого GO ТО следующим образом:
С CASE STATEMENT.
IF (KASE .LT. 1 . OR.KASE .GT. 4) KASE=5
GO TO (10, 20, 30, 40, 50), KASE С CASE 1. 10 CONTINUE
GO TO 200 С CASE 2. 20 CONTINUE
GO TO 200 С CASE 3
•C CASE n+1, ERROR CASE. 50 CONTINUE
200 CONTINUE С END CASE,
Этот метод может быть применен для обработки любого числа разветвлений. Предшествующий сегмент будет выполнять одну из частей программы в зависимости от выполнения условия. Операторы GO ТО используются для перехода вперед. Необходимо^ проверить, чтобы метки вычисляемого GO ТО не выходили за пределы допустимой области.
2.14,4. ВЫВОДЫ ПО СТРУКТУРНОМУ ПРОГРАММИРОВАНИЮ
Одно из преимуществ структурного программирования заключается в том, что программа может быть проанализирована путем проверки структуры, когда проектировщик или программист знакомит с программой своих коллег. Таким образом, становится возможным обнаружить ошибки уже на стадии программирования,, поэтому они обходятся сравнительно недорого. Процесс проверки структур подробно рассмотрен в гл. 5.
GO ТО: PROCEDURF OPTIONS (MAIN);
/* ЭТО ПРОГРАММА, В КОТОРОЙ СЛИШКОМ МНОГО ОПЕРАТОРОВ GO ТО.
ЧТО ПЕЧАТАЕТ ЭТА ПРОГРАММА? */ ' К = 0; GO ТО L4; L2: PUT LIST ('D') ; GO TO L3; L7: PUT LIST ('E') ; GO TO L5; L4: PUT LIST ('H') ; GO TO L7; L3: PUT LIST ('O') ; GO TO LI; L5: PUT LIST ('L') ; К = К + 1;
IF K<2 THEN GO TO L5; GO TO L3;
LI:
/* ИСПОЛЬЗУЕТ ЛИ ВАША ПРОГРАММА ОПЕРАТОРЫ GO ТО ПОДОБНЫМ ОБРАЗОМ? */
/* УКАЗАНИЕ: ГЛАВНЫЙ ПРОГРАММИСТ СЧИТАЕТ, ЧТО ИСПОЛЬЗОВАНИЕ ОПЕРАТОРОВ GO ТО В ВАШЕЙ ПРОГРАММЕ НЕЖЕЛАТЕЛЬНО */
END GO ТО;
Рис. 2.10. Многократное использование операторов GO ТО в программе.
При использовании структурного программирования существенно улучшается читаемость программы. Большие программы с самого начала разбивают на логические блоки подобно тому, как книгу делят на главы, а главы — на разделы. Каждый логический сегмент ограничен одной страницей программы с простой структурой, так что читателю несложно понять, что делает каждый сегмент. Сегмент имеет только один вход в начале и один выход в конце. И наконец, отсутствуют операторы GO ТО, поэтому программа может быть прочитана с начала до конца последовательно. На рис. 2.10 приведен "пример программы, в которой насчитывается слишком большое число операторов GO ТО.
Стремитесь к минимальному использованию операторов GO ТО.
В КОБОЛе и ФОРТРАНе трудно обойтись совсем без операторов GO ТО. Кроме того, программисты с большим опытом программирования на этих языках привыкли мыслить "в терминах <Ю ТО". Таким образом, стремясь избежать их использования, программист может оказаться в затруднительном положении. Вероятно, при работе с этими языками лучше всего ориентироваться на экономное применение указанных операторов. Для небольших программ можно допустить использование одного оператора GO ТО, для программ большего размера — двух-трех. Руководствуясь принципами структурного программирования, можно перепроектировать программу таким образом, чтобы убрать большую часть операторов GO ТО.
⇐2.13. Сложность || Оглавление || 2.15. Размышления о структурном программировании⇒
купить линзы acuvue, уникальное открытие психологов. |