33.4. Базовые управляющие структуры структурного программирования
В теории программирования доказано, что программу для решения задачи любой сложности можно составить только из трех структур, называемых следованием, ветвлением и циклом. Этот результат установлен Боймом и Якопини еще в 1966 году путем доказательства того, что любую программу можно преобразовать в эквивалентную, состоящую только из этих структур и их комбинаций.
Следование, ветвление и цикл называют базовыми конструкциями структурного программирования. Следованием называется конструкция, представляющая собой последовательное выполнение двух или более операторов (простых или составных). Ветвление задает выполнение либо одного, либо другого оператора в зависимости от выполнения какого-либо условия. Цикл задает многократное выполнение оператора (рис. 33.2). Особенностью базовых конструкций является то, что любая из них имеет только один вход и один выход, поэтому конструкции могут вкладываться друг в друга произвольным образом, например, цикл может содержать следование из двух ветвлений, каждое из которых включает вложенные циклы (рис. 33.3).
Рис. 33.2. Базовые конструкции структурного программирования
Рис. 33.3. Вложение базовых конструкций
Целью использования базовых конструкций является получение программы простой структуры. Такую программу легко читать (а программы чаще приходится читать, чем писать), отлаживать и при необходимости вносить в нее изменения. Структурное программирование называют «программированием без goto». Действительно частое использование операторов передачи управления в произвольные точки программы затрудняет прослеживание логики ее работы. Однако, есть ситуации, в которых использование goto оправдано и приводит, напротив, к упрощению структуры программы.
В большинстве языков высокого уровня существует несколько реализаций базовых конструкций. Они введены для удобства программирования, и в каждом случае надо выбирать наиболее подходящие средства.
← 33.3. Модульное программирование
33.5. Проектирование и тестирование программы →
33.5. Проектирование и тестирование программы
← 33.4. Базовые управляющие структуры.
33.6. Подпрограммы, процедуры и функции →
Структурный подход к программированию, как уже упоминалось, охватывает все стадии разработки проекта: постановка задачи (анализ требований), проектирование, собственно программирование (кодирование) и тестирование. Задачи, которые при этом ставятся, — уменьшение числа возможных ошибок за счет применения только допустимых структур, возможно более раннее обнаружение ошибок и упрощение процесса их исправления. Ключевыми идеями структурного подхода являются
- нисходящая разработка,
- структурное программирование и
- нисходящее тестирование.
Приведенные ниже этапы создания программ рассчитаны на достаточно большие проекты, разрабатываемые коллективом программистов. Для программы небольшого объема каждый этап упрощается, но содержание и последовательность этапов не изменяются. Постановка задачи (анализ требований). Создание любой программы начинается с этапа постановки задачи — анализа требований пользователей. Изначально задача ставится в терминах предметной области, и необходимо перевести ее в термины, более близкие к программированию. Поскольку программист редко досконально разбирается в предметной области, а заказчик — в программировании, постановка задачи может стать весьма непростым итерационным процессом. Кроме того, при постановке задачи заказчик зачастую не может четко и полно сформулировать свои требования и критерии. Постановка задачи завершается созданием технического задания, а затем внешней спецификации программы, включающей в себя:
- описание исходных данных и результатов (типы, форматы, точность, способ передачи, ограничения);
- описание задачи, реализуемой программой;
- способ обращения к программе;
- описание возможных аварийных ситуаций и ошибок пользователя.
Таким образом, программа рассматривается как черный ящик, для которого определена функция и входные и выходные данные. Проектирование (структурный анализ и проектирование). На этапе проектирования выявляется общая структура программы и взаимодействие модулей, а также разрабатываются структуры данных. На этом этапе применяется технология нисходящего проектирования программы, основная идея которого теоретически проста: разбиение задачи на подзадачи меньшей сложности — функциональные (логические) модули, которые можно рассматривать раздельно. Главный критерий разбиения (функциональной декомпозиции) — минимизация взаимодействия модулей. При этом используется метод пошаговой детализации. Можно представить себе этот процесс так, что сначала программа пишется на языке некоторой гипотетической машины, которая способна понимать самые обобщенные действия, а затем каждое из них описывается на более низком уровне абстракции, и так далее. Очень важной на этом этапе является спецификация интерфейсов, то есть способов взаимодействия подзадач. Для каждой подзадачи составляется внешняя спецификация, аналогичная приведенной выше. Для каждой подзадачи (модуля) описывается его функция, например, с помощью HIPO-диаграммы. Одна задача может реализовываться с помощью нескольких модулей и, наоборот, в одном модуле может решаться несколько задач. На более низкий уровень проектирования переходят только после окончания проектирования верхнего уровня. Для каждого модуля разрабатывается алгоритм обработки данных с помощью базовых конструкций структурного программирования и записывается в обобщенной форме — например, словесной, в виде обобщенных блок-схем или другими способами. Ниже приведён пример структурного проекта, описанный с помощью блок-схемы для логического модуля «1. 1. Перевод 10-ичного целого в р-ичное целое, представленное строкой» Рис. 33.1. Рис. 33.4. Блок-схема логического модуля «1. 1. Перевод 10-ичного целого в р-ичное целое, представленное строкой». Разработка внутренних структур данных. Большинство алгоритмов зависит от того, каким образом организованы данные, поэтому интуитивно ясно, что начинать проектирование программы надо не с алгоритмов, а с разработки структур, необходимых для представления входных, выходных и промежуточных данных. При этом принимаются во внимание многие факторы, например, ограничения на размер данных, необходимая точность, требования к быстродействию программы. Структуры данных могут быть статическими или динамическими. На этапе проектирования следует учитывать возможность будущих модификаций программы и стремиться проектировать программу таким образом, чтобы вносить изменения было возможно проще. Процесс проектирования является итерационным, поскольку в программах реального размера невозможно продумать все детали с первого раза. Кодирование (структурное программирование). На этапе кодирования выполняется структурное программирование программы. Процесс программирования также организуется по принципу «сверху вниз»: вначале кодируются модули самого верхнего уровня и составляются тестовые примеры для их отладки, при этом на месте еще не написанных модулей следующего уровня ставятся «заглушки» — временные программы. «Заглушки» в простейшем случае просто выдают сообщение о том, что им передано управление, а затем возвращают его в вызывающий модуль. В других случаях «заглушка» может выдавать значения, заданные заранее или вычисленные по упрощенному алгоритму. Таким образом, сначала создается логический скелет программы, который затем обрастает плотью кода. Можно применять к процессу программирования восходящую технологию — написать и отладить сначала модули нижнего уровня, а затем объединять их в более крупные фрагменты, но этот подход имеет ряд недостатков. Во-первых, в процессе кодирования верхнего уровня могут быть вскрыты те или иные трудности проектирования более низких уровней программы (просто потому, что при написании программы ее логика продумывается более тщательно, чем при проектировании). Если подобная ошибка обнаруживается в последнюю очередь, требуются дополнительные затраты на переделку уже готовых модулей нижнего уровня. Во-вторых, для отладки каждого модуля, а затем более крупных фрагментов программы требуется каждый раз составлять свои тестовые примеры, и программист часто вынужден имитировать то окружение, в котором должен работать модуль. Нисходящая же технология программирования обеспечивает естественный порядок создания тестов — возможность нисходящей отладки, которая рассмотрена далее. Этапы проектирования и программирования совмещены во времени: сначала проектируется и кодируется верхний уровень, затем — следующий, и так далее. Такая стратегия применяется потому, что в процессе кодирования может возникнуть необходимость внести изменения, отражающиеся на модулях нижнего уровня. Ниже приведён пример структурного кодирования проекта, описанный с помощью блок-схемы рис. 33.4. Модуль реализован в форме функции Object Pascal. Рис. 33.5. Текст программы логического модуля «1. 1. Перевод 10-ичного целого в р-ичное целое» на языке Object Pascal. Тестирование. На этом этапе выполняется нисходящее тестирование программы. Этот этап записан последним, но это не значит, что тестирование не должно проводиться на предыдущих этапах. Проектирование и программирование обязательно должны сопровождаться написанием набора тестов — проверочных исходных данных и соответствующих им наборов ожидаемых результатов. Необходимо различать процессы тестирования и отладки программы. Тестирование — это процесс выполнения программ с целью обнаружения логических ошибок [23]. Логической ошибкой считается любое не соответствие работы программы заданной на неё спецификации. Хорошим считается тест, который имеет высокую вероятность обнаружения еще не выявленной ошибки. Удачным считается тест, который обнаруживает еще не выявленную ошибку. Ниже приведён пример тестового набора данных для тестирования функции, реализующей логический модуль «1. 1. Перевод 10-ичного целого в р-ичное целое», представленной на рис.33.5. Будем тестировать функцию методом тестирования путей. Среди возможных путей выполнения функции можно выделить два пути выполнения, которые представлены на Е-схеме выполнения функции, полученной из блок-схемы. По первому пути цикл не выполняется ни разу. Второй путь включает в себя множество путей, на которых цикл выполняется один и более раз. Необходимо подобрать исходные данные для выполнения программы по каждому из возможных путей. Набор тестов для тестирования разработанной функции представлен в таблице.
Номер теста | Исходные данные | Ожидаемый результат | Путь прохождения |
1 | n = 0, p = 2 | 0 | Цикл не выполняется |
2 | n = 1, p = 2 | 1 | Цикл не выполняется |
3 | n = 8, p = 2 | 1000 | Цикл выполняется три раза |
4 | n = 0, p = 16 | 0 | Цикл не выполняется |
5 | n = 10, p =16 | A | Цикл не выполняется |
6 | n = 15, p =16 | F | Цикл выполняется три раза |
7 | n = 256, p = 16 | 100 | Цикл выполняется два раза |
Отладка программы — это процесс, осуществляемый после удачного выполнения теста. Процесс отладки начинается при обнаружении ошибки (например, при удачном завершении теста) и проводится в два этапа:
- определяется природа и местонахождение подозреваемой ошибки в программе;
- фиксируется или исправляется ошибка.
Отладка — процесс исправления ошибок в программе, при этом цель исправить все ошибки не ставится. Исправляют ошибки, обнаруженные при тестировании. При планировании следует учитывать, что процесс обнаружения ошибок подчиняется закону насыщения, то есть большинство ошибок обнаруживается на ранних стадиях тестирования, и чем меньше в программе осталось ошибок, тем дольше искать каждую из них. Для исчерпывающего тестирования программы необходимо проверить каждую из ветвей алгоритма. Кроме данных, обеспечивающих выполнение операторов в требуемой последовательности, тесты должны содержать проверку граничных условий (например, переход по условию х > 10 должен проверяться для значений, больших, меньших и равных 10). Отдельно проверяется реакция программы на ошибочные исходные данные. Идея нисходящего тестирования предполагает, что к тестированию программы приступают еще до того, как завершено ее проектирование. Это позволяет раньше опробовать основные межмодульные интерфейсы, а также убедиться в том, что программа в основном удовлетворяет требованиям пользователя. Только после того как логическое ядро испытано настолько, что появляется уверенность в правильности реализации основных интерфейсов, приступают к кодированию и тестированию следующего уровня программы.
←33.4. Базовые управляющие структуры. | 33.6. Подпрограммы, процедуры и функции→ |