Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Костюк - Основы программирования

.pdf
Скачиваний:
134
Добавлен:
30.05.2015
Размер:
1.3 Mб
Скачать

141

казчиком. После этого уточняется техническое задание и перепроектируется архитек­ тура.

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

Средства, с помощью которых записывают проекты, могут быть различными. В частности, это может быть тот же самый язык программирования, на котором будет написана вся программа, с тем отличием, что не детализированные действия в такой программе указываются особым образом, например, записываются в виде коммента­ рия на русском языке, см. в качестве примеров алгоритмы (7.11), (7.16), (7.21). Не­ редко при проектировании используют различные графические средства, тогда проект изображается, например, в виде набора блок-схем, аналогичных схемам на рис. 1.3 – 1.5. Кроме того, понять структуру программы помогает представление в графическом виде иерархии ее модулей.

Пример 8.1. Проект программы, вычисляющей определитель квадратной матри­ цы. Эта программа должна вводить входные данные, вычислять определитель и вы­

водить его значение. Для увеличения точности вычислений все вещественные пере­ менные должны иметь тип double.

Порядок ввода и структура входных данных:

1)величина, определяющая точность вычислений (диапазон от 10–30 до 10–2);

2)размер матрицы (диапазон от 1 до 50);

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

Порядок вывода выходных данных: вещественное значение определителя. Программа в целом имеет следующую структуру:

{Описания} begin

{Ввод входных данных} (8.1) {Вычисление определителя}

{Вывод выходных данных} end.

142

Модуль «Описания» содержит описания всех входных, выходных и внутренних переменных.

Модуль «Ввод входных данных» содержит операторы ввода входных данных в указанном выше порядке.

Модуль «Вычисление определителя» содержит алгоритм (7.16), в котором реали­ зован метод Гаусса вычисления определителя.

Модуль «Вывод выходных данных» содержит оператор вывода вычисленного значения определителя.

После такого определения модули второго уровня «Описания», «Ввод входных данных» и «Вывод выходных данных» легко запрограммировать.

Модуль второго уровня «Вычисление определителя» содержит записанные выше модули третьего уровня – алгоритмы (7.17), (7.18), (7.19) и (7.20).

На рис. 8.1 представлена иерархия модулей программы.

Программа

Описания

 

Ввод входных

 

Вычисление

 

Вывод выходных

 

данных

 

определителя

 

данных

 

 

 

 

 

 

 

 

 

 

 

Алгоритм

 

Алгоритм

 

Алгоритм

 

Алгоритм

(7.17)

 

(7.18)

 

(7.19)

 

(7.20)

 

 

 

 

 

 

 

Рис. 8.1

Конец примера.

В ранее применявшемся «стихийном методе» вначале выборочно программиру­ ют отдельные модули, после чего пытаются их скомпоновать в единую программу. В этом случае неизбежны многочисленные переделки из-за несогласованности моду­ лей, которых будет тем больше, чем сложнее программа и ее структура.

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

143

альная способность полностью решать все множество задач, на которые рассчитан комплекс.

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

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

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

8.4 Стиль и дисциплина программирования

Писать программы нужно так, чтобы в максимальной степени облегчить работу по сопровождению системы, когда она будет изучаться многими людьми и подвер­ гаться различным модификациям. Можно перечислить ряд требований, которым дол­ жен удовлетворять исходный текст программы, чтобы облегчить сопровождение си­ стемы:

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

144

текст программы должен быть хорошо откомментирован, так, чтобы коммента­ рии облегчили понимание структуры программы и структур используемых в ней дан­ ных;

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

Для удовлетворения перечисленным требованиям от программиста требуется определенная дисциплина. В частности, при программировании и написании текста программы рекомендуется придерживаться следующих правил:

1)писать программу необходимо с использованием отступов (пробелов) в начале каждой строчки текста, выделяя отступами блоки и группы операторов в программе так, чтобы начало и конец каждого оператора можно было легко увидеть в тексте, не следует писать слишком длинные строки текста;

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

3)рекомендуется придерживаться метода проектирования сверху-вниз, именно этот метод позволяет создавать хорошо структурированные программы, при этом размеры каждого модуля должны быть невелики, как правило, не более одной стра­ ницы (не более 50–100 строк текста);

4)проектируя модуль некоторого уровня, надо сначала полностью закончить его проект и только потом переходить к проектированию используемых им модулей нижних уровней;

5)если при проектировании модуля Ai некоторого уровня обнаружена ошибка или несогласованность в вызывающем его модуле Aj, то необходимо немедленно перейти к перепроектированию модуля Aj, и только затем повторить проектирова­ ние модуля Ai;

6)нужно стараться писать максимально простые и ясные модули, этому, в частности способствует необходимость доказательства правильности, когда прихо­ дится формулировать логические предусловия, постусловия и инварианты для каж­ дого оператора цикла или другой структурной единице в модуле;

7)для каждого модуля должны быть точно определены входные и выходные переменные, все другие переменные должны быть в модуле описаны локально;

8)лучше использовать хорошо известный и исследованный алгоритм в какомлибо конкретном модуле, чем изобретать свой собственный, благодаря этому ускоря­ ется проектирование и уменьшается число ошибок в системе;

9)действия по вводу-выводу желательно выделять в отдельные модули и не сме­ шивать их с вычислительными операциями.

145

За последние 30–40 лет в программировании сложились различные стили и дис­ циплины. Кратко рассмотрим наиболее важные и универсальные из них, которые в той или иной степени вошли в основу современных конкретных технологий разра­ ботки программного обеспечения.

Модульное программирование. Иного способа написать большую программу, кроме как разбить ее на модули, не существует. Однако не всякую программу, разде­ ленную на модули, следует считать модульной. Перечислим наиболее важные свой­ ства модульной программы:

размеры каждого модуля должны быть такими, чтобы средний программист, осуществляющий сопровождение программы, мог его целиком понять, отсюда огра­ ничение в 50–100 строк текста;

каждый модуль должен быть максимально независим от других модулей, воз­ можные изменения в одном модуле не должны привести к необходимости внесения изменений в другие модули.

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

Что касается программной реализации модулей, то возможны два способа: 1) в виде непосредственной вставки; 2) в виде процедуры (функции). Первый способ сле­ дует применять, если размеры модулей настолько малы, что даже после вставки об­ щий размер не превысит ограничение на длину модуля (см. пример 8.1). Второй способ более универсален. При этом описание процедуры (функции) можно объеди­

нить с программой статически (например, редактором связей) или динамически (например, создав динамически загружаемый модуль dll).

Структурное программирование. В настоящее время это общепринятый метод написания программ, хотя еще 25–30 лет назад его применение было скорее исклю­ чением, чем правилом, несмотря на то, что модульное программирование уже приме­ нялось. В то время в практическом программировании широко использовался «анти­ структурный» оператор goto, из-за которого, в частности, было невозможно дока­ зывать правильность программ.

Структурное программирование можно считать дальнейшим развитием модуль­ ного, когда внутренняя структура модуля состоит из таких структурных единиц, что у каждой есть только один вход и один выход. Для написания таких структурирован­ ных модулей вполне достаточно использовать последовательные, ветвящиеся и цик­

лические структуры, изображенные на рис. 1.3–1.5, или их эквиваленты (операторы case, циклы for и др.).

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

146

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

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

Таким образом, метод программирования с защитой от ошибок облегчает и уско­ ряет не только тестирование и отладку при разработке программы, но и последующее ее сопровождение.

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

147

Таблица решений – это такая таблица, в ячейках которой записаны условия и действия, а также указано, при каких комбинациях условий выполняются те или иные действия. Таблица решений делится на верхнюю и нижнюю части (верхняя часть относится к условиям, нижняя – к действиям). Кроме того, она делится на ле­ вую и правую части (в левой части записываются условия и действия, а в правой – значения условий и варианты выполнения действий). Если необходимо проверить k условий, то их всевозможные комбинации дадут 2k вариантов, которые все надо выписать для их осмысления и анализа! Ниже, на рис. 8.2, приведен пример таблицы решений с тремя условиями E1-E3 и пятью действиями D1-D5, выполняемыми в различных комбинациях при различных вариантах условий.

E1

И

И

И

И

Л

Л

Л

Л

E2

И

И

Л

Л

И

И

Л

Л

E3

И

Л

И

Л

И

Л

И

Л

D1

×

 

 

 

 

×

 

 

D2

 

 

×

 

 

 

 

 

D3

 

 

×

×

 

 

 

 

D4

 

 

 

 

×

 

 

 

D5

 

 

 

 

 

 

×

 

Рис. 8.2

Буква И (истина) обозначает выполнение условия, Л (ложь) – невыполнение. Знаком «×» отмечены исполняемые действия. При этом для одних вариантов условий может исполняться одно или несколько действий, а для некоторых других – ни одно­ го действия. Если требуется исполнять несколько действий, то они должны испол­ няться в том порядке, как записаны в таблице.

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

if E1 then begin

if E2 then begin if E3 then D1 end else begin

if E3 then D2; D3

end

end (8.2) else begin

if E2 then begin

if E3 then D4 else D1 end

else if E3 then D5 end;

148

Если проверяемые условия взаимно зависимы, то количество вариантов будет меньше, чем 2k, и тогда нет смысла вставлять в таблицу решений заведомо невыпол­ нимые варианты. Так, например, при сравнении между собой двух числовых пере­ менных x и y будет два условия, но только три варианта: x<y, x=y и x>y. При истинности условия x=y смысла проверять условие x<y (которое будет заведомо ложным). На рис. 8.3 изображен фрагмент таблицы решений, в котором знаком тире отмечен тот вариант условия, который не проверяется.

x=y

И

Л

Л

x<y

И

Л

Рис. 8.3

Пример 8.2. На плоскости заданы две вертикальные линии, пересекающие ось абсцисс в точках x1 и x2 (x1<x2), и две горизонтальные линии, пересекающие ось ординат в точках y1 и y2 (y1<y2). Эти линии делят плоскость на девять обла­ стей, перенумерованных числами от 1 до 9, как это показано на рис. 8.4.

 

 

 

1

 

 

 

 

2

 

 

 

3

 

 

 

 

 

 

 

 

 

 

 

 

y2

 

 

 

 

 

 

 

 

 

 

 

 

 

4

 

 

 

 

5

 

 

 

6

 

 

 

 

 

 

 

 

 

 

 

 

y1

 

 

 

 

 

 

 

 

 

 

 

 

 

7

 

 

 

 

8

 

 

 

9

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

x1

Рис. 8.4

 

x2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

x<x1

И

И

И

Л

Л

Л

 

Л

Л

Л

 

x>x2

И

И

И

 

Л

Л

Л

 

y<y1

И

Л

Л

И

Л

Л

 

И

Л

Л

 

y>y2

И

Л

И

Л

 

И

Л

 

L:=1

 

×

 

 

 

 

 

 

 

 

 

 

L:=2

 

 

 

 

 

 

 

 

 

×

 

 

L:=3

 

 

 

 

 

×

 

 

 

 

 

 

L:=4

 

 

 

×

 

 

 

 

 

 

 

 

L:=5

 

 

 

 

 

 

 

 

 

 

×

 

L:=6

 

 

 

 

 

 

×

 

 

 

 

 

L:=7

×

 

 

 

 

 

 

 

 

 

 

 

L:=8

 

 

 

 

 

 

 

×

 

 

 

L:=9

 

 

 

 

×

 

 

 

 

 

 

149

Рис. 8.5

Задана также точка с координатами x, y. Требуется переменной L присвоить номер той области, в которую попадает точка. На рис. 8.5 приведена таблица реше­ ний, в которой выполняются требуемые действия. Ее реализует алгоритм (8.3).

if x<x1 then begin if y<y1 then L:=7

else begin if y>y2 then L:=1 else L:=4 end end

else if x>x2 then begin if y<y1 then L:=9

else begin if y>y2 then L:=3 else L:=6 end (8.3) end

else begin

if y<y1 then L:=8

else begin if y>y2 then L:=2 else L:=5 end end;

Конец примера.

В целом технология использования таблиц решений следующая:

1)если при проектировании некоторого модуля в его алгоритме необходимо про­ верять несколько (три и более) условий в различных сочетаниях, то для этого целесо­ образно построить таблицу решений;

2)построенную таблицу решений необходимо проверить на полноту и непроти­ воречивость, при необходимости удалить лишние проверки условий;

3)по проверенной таблице решений записать алгоритм и тщательно проверить его на полное соответствие таблице;

4)таблицу решений следует включить в документацию сопровождения (описа­ ние программы) и сослаться на нее в комментарии к алгоритму.

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

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

Далее этот процесс повторяется для всех вызываемых модулей второго, затем третьего уровней и т.д. Таким способом можно доказать правильность всех модулей

150

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

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

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

8.5 Тестирование и отладка программных продуктов

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

Существует две технологии тестирования сложных программ: методом сверхувниз (нисходящая) и методом снизу-вверх (восходящая). Вторая кажется более есте­ ственной, именно она преимущественно использовалась вплоть до 1970-х годов. В настоящее время используют обе технологии, причем нередко при разработке очень сложной системы их применяют совместно. Различие в технологиях проявляется в том, в каком порядке тестируются модули в системе, как присоединяются новые мо­ дули. Что же касается тестирования отдельных модулей, то здесь применяются те же методы, что и для простых программ, описанные в первой главе: тестирование по ме­ тоду черного ящика и тестирование по методу белого ящика. При обнаружении оши­