Предпрограммная подготовка задачи



 

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

1. постановка задачи;

2. формирование математической модели задачи;

3. выбор и обоснование метода решения;

4. алгоритмизация вычислительного процесса;

5. программирование;

6. отладка и тестирование программы;

7. решение задачи на ЭВМ и анализ результатов;

8. сопровождение программы.

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

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

 Постановка задачи должна отвечать следующим требованиям:

- четкая формулировка цели с указанием вида и характеристик конечных результатов;

- представление значений и размерностей исходных данных;

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

- обозначения границы применимости и действия в случае выхода за них.

Формирование математической модели задачи - этап перевода словесной постановки задачи в совокупность математических зависимостей, описывающих исходные данные и вычисления промежуточных и конечных результатов.

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

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

Полученная математическая модель должна отвечать следующим требованиям:

- вначале составляется модель исходных данных, затем - расчетные зависимости;

- в модели исходных данных не изменяются размерности данных и не используются никакие математические операции;

- обозначение всех входящих в зависимости величин именами, определяющими их суть;

- указание размерностей всех используемых величин для контроля и дальнейшей модернизации решения;

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

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

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

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

2. Вместо констант[1] лучше использовать переменные[2]. Если в программе используются константы, то при их изменении нужно изменять в исходной программе каждый оператор[3], содержащий прежнюю константу. Эта процедура отнимает много времени и часто вызывает ошибки. В программе следует предусмотреть контроль вводимых данных (в частности, программа не должна выполняться, если данные выходят за пределы допустимого диапазона).

3. Некоторые простые приемы позволяют повысить эффективность программы (то есть уменьшить количество выполняемых операций и время работы программы). К таким приемам относится:

- использование операции умножения вместо возведения в степень;

- если некоторое арифметическое выражение встречается в вычислениях несколько раз, то его следует вычислить заранее и хранить в памяти ЭВМ, а по мере необходимости использовать;

- при организации циклов в качестве границ индексов[4] использовать переменные, а не выражения, которые вычислялись бы при каждом прохождении цикла;

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

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

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

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

- обеспечить проверку выполнения всех операций алгоритма;

- свести количество вычислений к минимуму.

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

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

Тестирование - это испытание, проверка правильности работы программы в целом, либо её составных частей.

Отладка и тестирование (англ. test - испытание) - это два четко различимых и непохожих друг на друга этапа:

- при отладке происходит локализация и устранение синтаксических ошибок и явных ошибок кодирования;

- в процессе же тестирования проверяется работоспособность программы, не содержащей явных ошибок.

Тестирование устанавливает факт наличия ошибок, а отладка выясняет ее причину. В современных программных системах (Turbo Basic, Turbo Pascal, Turbo C и др.) отладка осуществляется часто с использованием специальных программных средств, называемых отладчиками. Эти средства позволяют исследовать внутреннее поведение программы.

Программа-отладчик обычно обеспечивает следующие возможности:

- пошаговое исполнение программы с остановкой после каждой команды (оператора);

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

- установку в программе "контрольных точек", т.е. точек, в которых программа временно прекращает свое выполнение, так что можно оценить промежуточные результаты, и др.

При отладке программ важно помнить следующее:

- в начале процесса отладки надо использовать простые тестовые данные;

- возникающие затруднения следует четко разделять и устранять строго поочередно;

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

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

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

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

Тестовые данные должны обеспечить проверку всех возможных условий возникновения ошибок:

- должна быть испытана каждая ветвь алгоритма;

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

- первый тест должен быть максимально прост, чтобы проверить, работает ли программа вообще;

- арифметические операции в тестах должны предельно упрощаться для уменьшения объема вычислений;

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

- минимизация вычислений не должна снижать надежности контроля;

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

- усложнение тестовых данных должно происходить постепенно.

Процесс тестирования можно разделить на три этапа.

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

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

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

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

Как будет вести себя программа, работающая с массивами, если количество их элементов превысит величину, указанную в объявлении массива?

Что произойдет, если числа будут слишком малыми или слишком большими?

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

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

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

Сопровождение программы:

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

А также:

- доработка программы для решения конкретных задач;

- составление документации к решенной задаче, математической модели, алгоритму, программе по их использованию.

 

 

Модульное программирование

Изначальной и, вероятно, все еще самой общей парадигмой програм- мирования является следующая: «Решите, какие процедуры вы желаете; ис- пользуйте лучшие из алгоритмов, которые можете найти». При этом внимание фокусируется на определении процедуры: выбор алгоритма, необходимого для выполнения желаемых вычислений. Процедурное программирование использует функции для создания по- рядка в лабиринте алгоритмов. Когда Вы изучали язык программирования Си, Вы фактически изучали процедурное программирование. Одной из важнейших составляющих процедурного программирования является модульное программирование. Главную идею модульного програм- мирования можно сформулировать следующим образом. При решении задач выполняются одни и те же действия. Поэтому разумно реализовать такие операции один раз в виде некоторых модулей и в дальнейшем иметь возможность присоединять эти модули к своей программе. Как правило, модуль представляет собой совокупность функций и дан- ных, используемых для решения задач определенного класса или реализующих набор операций над некоторым понятием. Например, в одном модуле могут быть определены функции, обеспечивающие ввод-вывод. В другом модуле мо- гут содержаться функции, позволяющие выполнять операции над строками. Третий модуль поддерживает выполнение операций над геометрическими объектами. А еще один, например, обеспечивает доступ к информации в неко- торой базе данных. Если при разработке программы мы обнаружим, что нам необходимо вы- полнять операции над строками и работать с геометрическими объектами, то нет необходимости реализовывать эти подпрограммы заново. Достаточно под- ключить соответствующие модули к нашей программе. Кроме этого необходи- мо знать, а каким образом надо обращаться к этим модулям, чтобы решить наши задачи. Каждый модуль, входящий в состав программы, решает задачи из своей области. Поэтому модули могут и должны разрабатываться отдельно друг от друга. Это позволяет вести коллективную разработку программной системы. Отдельные программисты или небольшие коллективы независимо от других разрабатывают свою часть программной системы. Кроме того, такой подход обеспечивает возможность модульной отладки, при которой отлаживается не вся программная система в целом, а отдельные ее компоненты. Такая техноло- гия обеспечивает быструю локализацию и устранение ошибок, что приводит к ускорению разработки программного продукта. Основным стержнем, на котором держится модульное программирова- ние, является аппарат подпрограмм (функций). Внутри модулей действия реа- 4лизуются посредством определения соответствующих подпрограмм. В вызывающей программе содержатся фактически только вызовы необходимых подпрограмм. Программные средства поддержки этой технологии наиболее полно впервые были реализованы в языке программирования Фортран и потом в подавляющем большинстве последующих языков. Для того чтобы воспользоваться средствами, предоставляемыми модулем, мы должны иметь некую дополнительную информацию по этому модулю. Во- первых, мы должны знать, какие подпрограммы какие действия выполняют. Во- вторых, мы должны знать, какую информацию мы должны передать этим подпрограммам для решения задачи, и каким образом она должна быть передана. Таким образом, важным моментом при использовании модульного программирования является обеспечение взаимодействия между модулями, или как говорят, определение интерфейса. Описание интерфейса, как правило, приводят в специальном заголовочном файле. Передача информации между подпрограммами (функциями) модулей в большинстве случаев осуществляется через список параметров при вызове соответствующей подпрограммы. Иногда допускается использование глобальных переменных, но каждый такой случай должен быть обоснован. Для рассматриваемой технологии характерна еще одна задача, а именно задача сборки программы из модулей. Существует несколько способов её ре- шения, некоторые из которых рассматриваются далее. Наиболее распространённым способом сборки программы является сбор- ка на уровне объектных файлов. Суть его состоит в том, что для каждого раз- работанного модуля, посредством компилятора с языка, отдельно получают объектный файл, а затем, используя компоновщик, объединяют их в единый исполняемый образ программы. Такие известные языки программирования как язык ассемблера, Си, Фортран поддерживают данную технологию. Однако, например, не менее известный язык Паскаль в его классическом варианте такой возможности не предусматривает. На рис.1 приведена иллюстрация рассмот- ренного метода. В современных вычислительных системах, поддерживающих многоза- дачность, широкое распространение получила новая концепция динамической 5 Рис. 1. Сборка на уровне объектных файлов module1.c module2.c moduleN.c . . . module1.o module2.o moduleN.o . . . programсборки программы. В её основе лежит использование разделяемых библио- тек (so) или библиотек динамического связывания (dll). Механизм динами- ческого связывания позволяет программе вызвать функцию, которая не является частью этой программы. Исполнимый код такой функции находится как раз в разделяемой библиотеке, которая компилируется и хранится отдельно от про- грамм, использующих ее. Различают два способа динамического связывания: связывание времени загрузки (load-time linking) и связывание времени исполнения (run-time linking). Первый случай имеет место, когда в программе производится явное обращение к функции из внешней библиотеки. В этом случае при создании программы используется специальная библиотека импорта (import library). Та- кая библиотека содержит информацию о функциях самой библиотеки, но не содержит исполнимого кода этих функций. Во время загрузки программы, опе- рационная система использует информацию из библиотеки импорта, чтобы определить расположение исполнимого кода разделяемой библиотеки и загру- зить его в память компьютера. Связывание времени исполнения используется в том случае, когда про- грамме во время своего выполнения может потребоваться получить доступ к библиотекам, неизвестным на этапе разработки программы. При этом сама программа может управлять процессом загрузки и выгрузки таких библиотек, а обращение к их функциям производится неявным способом, используя специ- альные точки входа. Такой механизм обусловлен тем, что на этапе разработки программы имя вызываемой функции может быть неизвестно, а, следователь- но, Вы не можете произвести к ней явное обращение вида myFunction(); а должны использовать конструкцию вида: void* handle = dlopen("libm.so", RTLD_LAZY); void (*myFunction)(); //myFunction — указатель на функцию *(void **) (&myFunction) = dlsym(handle, "myFunction"); (*myFunction)(); 1.2. Характеристики модулей Важнейшим принципом разработки модуля является принцип информа- ционной закрытости. Этот принцип утверждает, что содержание модулей должно быть скрыто друг от друга. Модуль должен определяться и проектиро- ваться так, чтобы его содержимое (функции и данные) было недоступно тем модулям, которые не нуждаются в такой информации (клиентам). Этот принцип иллюстрируется на рис.2. Информационная закрытость означает, что все модули независимы и об- мениваются только необходимой для работы информацией. Кроме этого доступ к операциям и структурам данных модуля извне его ограничен. Следование принципу информационной закрытости даёт определённые 6преимущества. С одной стороны обеспечивается возможность разработки мо- дулей независимыми разработчиками, что уменьшает время разработки про- граммной системы. Но он даёт преимущества и в малых проектах, когда коли- чество разработчиков невелико, или он вообще один. Это преимущество за- ключается в лёгкой модификации системы. При использовании принципа веро- ятность распространения ошибок сильно уменьшается, так как многие данные и функции локализованы внутри модуля и скрыты от других частей системы. В этом случае модуль играет роль «чёрного ящика». Его содержимое (реализация) недоступно другим модулям, а управление им (интерфейс) про- стое. Изменение реализации модуля никоим образом не может повлиять на его пользователей. Для оценки уровня информационной закрытости модуля можно исполь- зовать его внутреннюю характеристику - связность. Под связностью модуля понимается мера зависимости его частей. Чем выше связность модуля, тем луч- ше результат проектирования. Далее приведены различные типы связности в порядке её уменьшения. При этом первые три свидетельствуют о хорошем ка- честве модуля, а остальные о недостаточно высоком качестве разработки.

●Функциональная связность. Части модуля вместе реализуют одну проблемную задачу (операцию). Например: вычисление синуса угла, проверка орфографии, вычисление зарплаты сотрудника, определение места пассажира. Модуль выполняет только то, для чего он предназначен. Поэтому модуль вы- числения синуса не должен печатать его значение.

●Информационная связность. Выходные данные одной части использу- ются как входные данные в другой части модуля. Порядок выполнения дей- ствий строго определён и подобен конвейеру.

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

●Процедурная связность. Этот тип связности соответствует случаю, когда 7 Рис. 2. Информационная закрытость модуля Интерфейс Реализация модули-клиенты Модульчасти модуля связаны порядком выполняемых ими действий, реализующих некоторый сценарий поведения. При этом зависимость по данным между частями модуля отсутствует.

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

●Логическая связность. В этом случае части модуля объединены по принципу функционального подобия. Например, модуль включает в себя раз- личные функции обработки ошибок. К серьёзным недостаткам таких модулей относится сложное сопряжение с другими модулями и большая вероятность внесения ошибок при необходимости изменений.

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

●Сцепление по данным. В этом случае функции модуля А вызывают функции модуля B, причём все входные и выходные параметры вызываемого модуля являются простыми элементами данных.

●Сцепление по образцу. Здесь в качестве параметров используются структуры данных.

●Сцепление по управлению. Один модуль явно управляет функциониро- ванием другого модуля с помощью флагов или переключателей, посылая ему управляющие данные.

●Сцепление по внешним ссылкам. В этом случае оба модуля ссылаются на один и тот же глобальный элемент данных.

●Сцепление по общей области. Здесь модули разделяют одну и ту же глобальную структуру данных.

●Сцепление по содержанию. Один модуль прямо ссылается на содержа- ние другого модуля (в обход интерфейсной части вызываемого модуля).

Соглашения по разработке модулей За время существования такой дисциплины как программирование, в ней были выработаны определенные правила оформления программных модулей. Подавляющее большинство разработчиков программного обеспечения стре- мятся их соблюдать. Следование этим правилам позволит получить хорошо структурированную, легко читаемую программу. Использование одних и тех же правил оформления модулей позволяет легче и быстрее разобраться в них, значительно уменьшает количество «необязательных» ошибок. Те же, кто эти правила совершенно игнорируют, всех этих преимуществ лишены. На этапе обучения следование приведённым далее правилам обязательно. Вместе с тем нельзя рассматривать их как некие догмы. Это всего лишь рекомендации. В процессе трудовой деятельности вы вынуждены будете их несколько адаптиро- 8вать в соответствии с требованиями трудовой организации, компании. 1. Проектируйте модуль со связностью не ниже коммуникативной. Он не должен быть просто совокупностью никак не связанных между собой функций и данных. 2. Модуль должен охватывать, как можно большую часть задач рассматри- ваемой области, и в то же время набор доступных подпрограмм следует сделать минимальным и простым в использовании. 3. Добейтесь, чтобы сцепление модуля было по данным или по образцу. В противном случае изменение некоторого модуля может вызвать лавинообразные изменения в других модулях. 4. При разработке модуля с именем Name, как правило, создается два файла: файл-заголовок Name.h и файл-реализация Name.cpp. В файле-за- головке определяется интерфейс к данному модулю. Он включает в себя опре- деление констант, новых типов данных, описаний функций с обязательным указанием их параметров и описаний глобальных переменных, определенных в модуле и доступных для других частей программы. Если модуль «A» обраща- ется к модулю «B», то в модуле «A» должен быть подключен файл-заголовок модуля «B». Поскольку файл-заголовок может подключаться несколько раз, то во избежание дублирования используют конструкцию следующего вида #ifndef name_h #define name_h //... //описания //... #endif Файл-реализация Name.cpp содержит определения глобальных и стати- ческих переменных модуля, определение новых типов данных, используемых только внутри этого модуля и реализации функций. Очевидно, что в этом мо- дуле должно производиться подключение файла-заголовка. 5. Минимизируйте использование глобальных переменных. Наилуч- шим решением является отказ от их использования. Поскольку глобальные переменные могут быть сделаны доступными из любого модуля, то чрезвычай- но трудно контролировать их использование. Настоятельно рекомендуется передавать в функцию все необходимые ей данные в качестве параметров. 6. Избегайте слишком больших реализаций функций. Например, можно потребовать, чтобы вся реализация функции умещалась в определённое коли- чество строк (количество строк на экране монитора, в пределах одного листа при печати и т .д.). Кроме этого важно избегать глубоких вложений внутри функции (не более 2 вложенных операторов). Игнорирование этой рекоменда- ции приводит к более трудному восприятию программного текста, реализую- щего данную функцию, а что еще более важно - к повышению вероятности со- вершить ошибку при кодировании алгоритма. 7. Каждая строка программы должна содержать не более одного оператора. Также избегайте объявления нескольких переменных на одной строке. 98. Программный код пишется для чтения не компилятором, а человеком. Программный код должен сам себя документировать и средства языка C позво- ляют обеспечить выполнение этого требования. Как минимум, все используе- мые в программах имена должны быть содержательными. Понять смысл оператора digitCount = digitCount + 2 гораздо проще, чем c = c+2. 9. Хорошая практика программирования состоит в том, чтобы всем явным константам давать символические имена и их использовать в программе. Указанное правило не относится к константам, которые даже в принципе не могут поменяться. Например, индексация элементов в векторе начинается с 0, поэтому определение для этой константы символического имени бессмысленно и только запутывает программный код. 10. Используйте отступы для выделения операторов и их блоков внутри условного оператора, оператора цикла и выбора. Следование этой реко- мендации позволяет избежать ошибок, связанных, например, с расстановкой скобок { и }. Но еще более важно, что при таком подходе четко прослеживается структура программы. 1.4. Пример: операции над полиномами В качестве примера, иллюстрирующего «хороший стиль» программиро- вания, рассмотрим следующую задачу. Пусть требуется реализовать набор функций для выполнения операций над полиномами переменной степени Pn  x=an x nan−1 x n−1a1 xa0 . В рамках нашего примера ограничимся операциями ввода полинома со стандартного устройства ввода и вывода на стандартное устройство вывода. Для иллюстрации использования реализован- ных операций рассмотрим тестовую программу циклического ввода-вывода полинома. Примем, что необходимость повторного ввода полинома определя- ется по команде пользователя. Если пользователь в ответ на запрос о продол- жении работы с программой ввел y, то необходимо продолжить, в противном случае программа завершается. Из определения понятия «полином» следует, что он обладает следующи- ми характеристиками: • степень полинома; • набор коэффициентов полинома. Для представления понятия полинома определим тип данных Polinom, представляющий собой структуру с указанными выше полями. С целью упро- щения примера введем дополнительное ограничение: максимально возможную степень полинома n max . Конечная программа будет состоять из трех файлов. В файле polinom.h содержится определение структуры описания полинома, в файле polinom.cpp содержатся реализации функций над полиномом, а в файле main.cpp приводит- ся текст основной программы. //polinom.h 10//файл содержит определение структуры данных //для описания полинома #ifndef POLINOM_H #define POLINOM_H //максимальная степень полинома const int PolinomMaxDegree = 10; //структура описания полинома struct Polinom { int n; //степень полинома double a[PolinomMaxDegree+1]; //коэффициенты полинома }; extern int PolinomRead(Polinom *p); ///функция ввода полинома extern void PolinomWrite(Polinom *p); ///функция вывода полинома #endif //polinom.cpp //файл содержит реализации функций над полиномом #include "polinom.h" int ReadPolinom(Polinom *p) { int n; printf("Введите степень полинома:"); scanf("%d",&n); if( nPolinomMaxDegree ) return -1; printf("Введите коэффициенты полинома:"); for( int i = n; i >= 0; --i ) scanf("%lf",&p->a[i]); //старший коэффициент должен быть !=0 if( p->a[n]==0 ) return -1; p->n=n; return 0; } void WritePolinom(Polinom *p) { printf("Коэффициенты полинома: "); for( int i = p->n; i >= 0; --i) 11printf("%lg ",p->a[i]); printf("\n"); return; } //main.cpp - Программа тестирования ввода-вывода полиномов. //Версия 1.0 //Разработчик: Иванов И.И. //Дата разработки: 25 декабря 2000 г. #include #include "polinom.h" extern int NeedContinue(); int main() { char appName[] ="Программа ввода-вывода полиномов. Версия 1.0\n"; Polinom pol; //исходный полином puts(appName); //напечатать назначение программы do { if( ReadPolinom(&pol) != -1 ) WritePolinom(&pol); else printf("Некорректно заданы значения параметров\n"); } while( NeedContinue() ); return 0; } int NeedContinue() { printf("продолжать?(y/n):"); return getchar() == 'y'; } 1.5. Технология проектирования сверху-вниз 1.5.1.Постановка задачи При создании сложных программных систем важной задачей является правильная организация процесса проектирования системы. Одним из самых распространенных методов решения этой задачи является технология проек- тирования «сверху вниз». Этот метод напрямую связан с технологией мо- дульного программирования. Проектирование и реализация программной системы возникают не сами 12по себе, а как следствие постановки некоторой задачи, которую требуется ре- шить. При этом, формулируя задачу, мы имеем дело с понятиями реального мира, а не с абстрактными числами, строками, функциями и т. д. Например, в задаче рисования мы имеем дело с понятиями «картинка», «инструмент рисо- вания» и т. д. Над объектами, представляющими понятие, можно совершать различные действия, либо они сами могут выполнять какие-либо операции. Например, по- лученную картинку можно сохранить для дальнейшего использования, открыть ее для просмотра, вставить в документ, редактировать ее, вставляя и удаляя геометрические фигуры и т. д. Во многих случаях понятие может быть определено, используя ряд дру- гих, более простых понятий. Возвращаясь к нашему примеру, «картинку» мож- но определить как совокупность различных геометрических фигур, таких как точки, линии, окружности. В этом случае понятие «геометрическая фигура» выступает как понятие более низкого уровня, и используется для определения «картинки». В свою очередь понятие геометрической фигуры можно также де- тализировать используя перечисленные выше понятия. Объект вместе с операциями, связанными с ним, удобно считать неким исполнителем с определенной системой предписаний (действий). При этом исполнителя можно рассматривать с внешней и внутренней точек зрения. Вы- полнение любой программы состоит в проведении каких-то действий над каки- ми-то объектами, то есть в использовании исполнителей. При использовании исполнителя интересны его внешние свойства, а именно, какие у него есть предписания, что получается в результате их выполнения и т. д. Описание этих свойств называется внешним описанием исполнителя. Оно позволяет им поль- зоваться, но ничего не говорит о его внутреннем устройстве, о его реализации. Но для того чтобы исполнителя можно было использовать, необходимо иметь его реализацию, причем возможно на базе других исполнителей. Таким образом, при реализации мы рассматриваем исполнителя с внутренней точки зрения. На этом этапе нам интересна его структура и как он выполняет те или иные действия. При этом реализация каждого из исполнителей может рассмат- риваться как отдельная подзадача в рамках исходной задачи. При проектировании программной системы у разработчика имеется в распоряжении ряд исполнителей, уже реализованных ранее, либо под- держиваемых самой системой программирования. Такие исполнители назовем базовыми исполнителями. Например, базовыми исполнителями можно считать библиотеку файлового ввода-вывода, математическую библиотеку, по- нятие массива с определенными над ним операциями, либо некоего исполните- ля, реализованного Вами ранее. Таким образом, задачу разработки программ- ной системы можно определить как задачу реализации искомого исполнителя A на основе совокупности исполнителей E1 ,, Ek . Реализацию A на базе указанной совокупности исполнителей будем обозначать как RA , E1 ,, Ek  . 131.5.2.Декомпозиция задачи Как мы с Вами установили, исходную задачу удобно решать, разбивая ее на подзадачи с помощью промежуточных исполнителей. Технология про- граммирования «сверху вниз» предлагает такое разбиение вести от исполнителя A (сверху) к базовым исполнителям E1 , E2 ,, Ek (вниз). В соответствии с указанным подходом сначала выделяется и реализуется общая идея алгоритма, и только потом прорисовываются его детали. При этом каждый раз, делая очередной шаг вниз, формулируются уточнения. Схематиче- ски результаты проведения этого процесса можно представить следующим об- разом: Таким образом, модульная структура программы имеет вид графа (в при- веденном примере - древовидную структуру). В узлах такого дерева размеща- ются программные модули, а направленные дуги (стрелки) показывают стати- ческую подчиненность модулей, т. е. каждая дуга показывает, что в тексте мо- дуля, из которого она исходит, есть ссылка на модуль, в который она входит. Процесс разбиения задачи на ряд подзадач носит название декомпози- ции. Конечная программа состоит из нескольких слоев и представляет собой иерархию исполнителей. При этом все или некоторые из них могут быть уже реализованы ранее, а, следовательно, достаточно их подключения к основной программе. Остальные же исполнители необходимо реализовывать, при этом они, в свою очередь, могут быть реализованы с привлечением других исполни- телей. Проводя декомпозицию задачи, необходимо руководствоваться скорее тем, что надо сделать, нежели тем, на какой базе. То есть, при использовании технологии проектирования «сверху-вниз», Вы создаете реализацию некоторо- го исполнителя в удобном, естественном виде, не привязываясь жестко к уже существующим исполнителям. Технологическая цепочка проектирования «сверху-вниз» приведена на рис.4. Центральным моментом технологии «сверху-вниз» является шаг де- композиции. Эта операция состоит в том, чтобы по имеющемуся внешнему описанию исполнителя A получить его реализацию на базе придуманного нами исполнителя B и одновременно внешнее описание придуманного испол- нителя B . 14 Рис. 3. Результат проведения декомпозиции A B1 E3 B2 E1 C1 E2 C2 E6 E4 E5Шаг декомпозиции можно провести различными способами. Придумывая нового исполнителя B , следует учитывать несколько требований. С одной стороны, необходимо как можно большую часть работы переложить на нового исполнителя B . Вместе с тем проведенный шаг декомпозиции должен прибли- жать нас к базовым исполнителям E . И, конечно же, придуманный исполни- тель должен соответствовать некоему реальному понятию. Обычно, при проведении декомпозиции, исполнители реализуются на базе нескольких исполнителей, а не одного. В этом случае в результате очеред- ного шага появляются внешние описания этих исполнителей и одна реализация R A , B1 ,, Bk  . Некоторые из этих исполнителей могут быть базовыми, то есть Bi ∈{E1 ,, Ek } , либо получены ранее, на предыдущих шагах. Очевидно, что если все исполнители удовлетворяют этим условиям, то процесс проведе- ния декомпозиции можно считать завершенным. При проведении декомпозиции мы можем увидеть, что придуманная нами иерархия промежуточных исполнителей неудачна, что какой-то исполни- тель не реализуется или реализуется очень сложно, что внешнее описание ис- полнителя необходимо уточнить или изменить. В этом случае приходится воз- вращаться на несколько шагов назад и начинать сначала. Таким образом, проектирование и программирование являются итеративными (повторяющимися) процессами. Среди всего прочего проектирование «сверху-вниз» обладает существен- ным преимуществом. При его использовании удается совместить процесс от- ладки с процессом разработки. Пусть, например, получена реализация A , а реализации некоторых из промежуточных исполнителей отсутствуют. В этом случае нереализованные части программы заменяются имитаторами (заглуш- ками). Каждый имитатор модуля представляется достаточно простым про- граммным фрагментом, который, в основном, сигнализирует о самом факте об- ращения к имитируемому модулю, производит необходимую для правильной работы программы обработку значений его входных параметров (иногда с их распечаткой) и выдает, если это необходимо, заранее подготовленный подходя- щий результат. После завершения тестирования и отладки реализации исполнителя A производится переход к реализации и последующему тестированию одного из исполнителей, которые в данный момент представлены имитаторами, если та- ковые имеются. Для этого имитатор выбранного для тестирования исполнителя заменяется его реализацией и, кроме того, добавляются имитаторы тех испол- 15 Рис. 4. Технологическая цепочка процесса декомпозиции А B R(А,B) Шаг декомпозиции E R(Z,E) . . .нителей, к которым может обращаться выбранный для тестирования исполни- тель. Кроме того, очень удобно реализацию каждого исполнителя производить в своем модуле, а его внешнее описание в соответствующем этому модулю за- головочном файле. Если исполнители A и B используют исполнителя C в качестве промежуточного, то доступ A и B к внешнему описанию C можно обеспечить простым подключением файла-заголовка. Хранение внешних описаний всех исполнителей в одном файле-заголовке может показаться рациональным. Однако такой подход неприемлем хотя бы в силу того, что исполнители уже фактически не отделены друг от друга. При разработке другой программной системы мы можем пожелать воспользоваться некоторыми из реализованных ранее исполнителей. Однако поскольку их внешние описания объединены, то мы вынуждены будем использовать все, а не только нужные нам. Кроме того, при изменении внешнего описания какого- либо исполнителя, повторную компиляцию придется провести для всех ис- полнителей. В случае же отдельных файлов-заголовков компиляция потребуется только для тех реализаций, которые имеют дело с изменившимся исполнителем. Этот фактор особенно важен при разработке больших программных систем. Как Вы могли обратить внимание, рассмотренная схема не предполагает обязательного проведения всей декомпозиции программной системы на этапе проектирования до начала программирования. Представляется сомнительным, чтобы до программирования модулей можно было разработать структуру всей программы точно и содержательно. Наоборот, часто более надежным методом оказывается одновременное проведение декомпозиции и получение реализаций исполнителей.

 

 

Стиль программирования

Технология программирования - это совокупность методов и средств раз- работки (написания) программ и порядок применения этих методов и средств. В настоящее время технологии программирования разделяют по исполь- зуемым стилям программирования на процедурное (структурное), функцио- нальное, логическое и объектно-ориентированное программирование. Они различаются по уровню абстракции данных, используемым моделям вычисле- ний, классам решаемых задач. Ниже дана характеристика перечисленных стилей программирования. 1.1. Процедурное программирование Процедурное (императивное) программирование является отражением архитектуры традиционных ЭВМ, предложенной фон Нейманом в 40-х годах. Процедурная программа состоит из последовательности операторов и пред- ложений, управляющих последовательностью их выполнения. Типичными операторами являются операторы присваивания и передачи управления, опе- раторы ввода/вывода и специальные предложения для организации циклов. Из них можно составлять фрагменты программ и подпрограммы. В основе такого программирования лежит взятие значения какой-то переменной, совершение над ним действия и сохранение нового значения с помощью оператора при- сваивания, и так до тех пор пока не будет получено (и, возможно, напечатано) желаемое окончательное значение. Знакомый многим пример неимперативного программирования - элек- тронная таблица. В ней значения ячеек задаются выражениями, а не команда- ми, определяющими, как вычислять это значение. Нигде также не задается по- рядок вычисления значений ячеек, гарантируется, что вычисления будут вы-5 полнены в правильном порядке с учетом зависимости ячеек друг от друга. В электронной таблице не используется присваивание, то есть указание изме- нить текущее значение ячейки. Если мы не управляем сами последовательно- стью вычислений, то мы и не знаем, когда произойдет присваивание, а поэто- му от него мало пользы. К процедурным языкам относятся Basic, Cobol, Fortran, Pascal, C и Ada. 1.2. Структурное программирование Структурный подход к разработке ИС заключается в ее декомпозиции (разбие- нии) на автоматизируемые функции: система разбивается на функциональные подсистемы, которые в свою очередь делятся на подфункции, подразделяемые на задачи и подзадачи. Структурное программирование основано на следующих принципах: - программирование должно осуществляться сверху-вниз; - весь проект должен быть разбит на модули с одним входом и одним выходом ; - логика алгоритма и программы должна допускать только три основные структу- ры - последовательное выполнение, ветвление и повторение. Недопустим оператор передачи управления в любую точку программы; - при разработке документация должна создаваться одновременно с программиро- ванием, в виде комментариев к программе. Цель структурного программирования - повышение надежности программ, обеспечение сопровождения и модификации, облегчение и ускорение разработки. Идеи структурного программирования появились в начале 70-годов в компании IBM, в их разработке участвовали такие известные ученые как Э. Дейкстра, Х. Милс, Э. Кнут, С. Хоор. 6 1.3. Функциональное программирование Программа, разработанная с использованием функционального (апплика- тивного) стиля, состоит из совокупности определений функций. Функции, в свою очередь, представляют собой вызовы других функций и предложений, управляющих последовательностью вызовов. Вычисления начинаются с вызо- ва некоторой функции, которая в свою очередь вызывает функции, входящие в ее определение и т. д. в соответствии с иерархией определений и структурой условных предложений. Функции часто либо прямо, либо опосредованно вы- зывают сами себя. Каждый вызов возвращает некоторое значение в вызвавшую его функ- цию, вычисление которой после этого продолжается; этот процесс повторяет- ся до тех пор, пока запустившая вычисления функция не вернет конечный ре- зультат пользователю. "Чистое" функциональное программирование не признает присваиваний и передач управления. Разветвление вычислений основано на механизме обра- ботки аргументов условного предложения. Повторные вычисления осуществ- ляются через рекурсию, являющуюся основным средством функционального программирования Первый функциональный язык программирования (Лисп) был разработан американским ученым Дж. Маккарти (J. McCarthy) в 1958-1961 гг. на основе алгебры списочных структур, лямбда-исчисления и теории рекурсивных функций. К настоящему времени созданы такие функциональные языки про- граммирования как Scheme, Рефал, Haskell, Sisal. 7 1.4. Логическое программирование Логическое (реляционное) программирование исходит из того, что компью- тер должен уметь работать по логическим построениям, присущим человеку. На- пример, в логическом программировании разрешена конструкция типа "Опреде- лить фирму, имеющую самую высокую в городе среднюю зарплату сотрудников", которой достаточно, чтобы получить ответ. Программа в таких языках представляет собой совокупность правил (оп- ределяющих отношения между объектами) и цели (запроса). Процесс выпол- нения программы трактуется как процесс установления общезначимости ло- гической формулы по правилам, установленным семантикой того или иного языка. Результат вычислений является побочным продуктом процедуры выво- да. Такой метод являет собой полную противоположность программирования на каком-либо из процедурных языков. В реляционном программировании нужно только специфицировать факты, на которых алгоритм основывается, а не определять последовательность шагов, которые необходимо выполнить. Примером логического языка программирования можно назвать PROLOG - язык, предназначенный для программирования приложений, использующих средства и методы искусственного интеллекта, создания экспертных систем и представления знаний. 1.5. Объектно-ориентированное программирование Объектно-ориентированная технология разработки программ состоит из объектно-ориентированного анализа, объектно-ориентированного проектирования и объектно-ориентированного программирования. Объектно-ориентированный анализ состоит в объектной декомпозиции предметной области, т.е. информационная система представляется не набором8 Выполнить заказ Занести в план Оформить на- кладную Отпустить товар Проверить на- личие товара на складе Изменить количество товара на складе Рисунок 1. – Фрагмент функциональной схемы системы складского учета. Включить то- вар в наклад- ную Передать товар за- казчику функций, а совокупностью объектов, взаимодействующих друг с другом. Деком- позиция – это разделение сложной программной системы на все меньшие и мень- шие подсистемы, каждую из которых можно совершенствовать независимо[1]. Структурное проектирование предполагает алгоритмическую декомпозицию, по- нимаемую как разделение алгоритмов, где каждый модуль системы выполняет один из этапов общего процесса. На рис.1 показан фрагмент функциональной схе- мы для системы складского учета. Эту схему можно рассматривать как пример ал- горитмической декомпозиции. Приведенной на рис. 1 декомпозиции существует альтернатива, представ- ленная на рис.2. Здесь предметная область представлена как совокупность неко- Заказ Товар Накладная включить Склад Проверить наличие Изменить количество оформить принять Рисунок 2.-Объектно-ориентированная декомпозиция Заказчик передать9 торых автономных объектов, которые взаимодействуют друг с другом, чтобы обеспечить функционирование всей системы в целом. Объекты обладают поведением, состоянием, свойствами, которые в про- грамме реализуются в виде подпрограмм (функций). Таким образом, объектно- ориентированная технология включает в себя возможности структурного подхода, но объектно-ориентированное проектирование в большей степени реализует мо- дель реального мира и соответствует естественной логике человеческого мышле- ния. По мнению автора С++, Бьерна Страуструпа[2], различие между проце- дурным и объектно-ориентированным стилями программирования заключается примерно в следующем: программа на процедурном языке отражает "способ мышления" процессора, а на объектно-ориентированном - способ мышления про- граммиста. Отвечая требованиям современного программирования, объектно- ориентированный стиль программирования делает акцент на разработке новых типов данных, наиболее полно соответствующих концепциям выбранной области знаний и задачам приложения. Сравнивая объектно-ориентированный и процедурный стиль программирова- ния (подробно остановимся на этих двух технологиях, поскольку для остальных характерна некоторая функциональная ограниченность, не позволяющая исполь- зовать их для решения широкого круга задач), необходимо выбрать критерий сравнения. Основной критерий в оценке программных продуктов – сложность[1] , а основными требованиями к методологиям разработки являются: удобство со- провождения, возможность безболезненного наращивания уже существующей программы, способность разработанных программных объектов к повторному ис- пользованию. При этом на второй план отступает такое требование, как быстрое проектирование первоначальной версии программы, потому что его воплощение обычно не позволяет соблюсти все остальные условия. Дело в том, что процесс разработки программного обеспечения не заканчивается первой версией. Он сво- дится к итеративному расширению предыдущих версий, что, в некоторой степени, 10 и помогает решать проблему сложности. В борьбе с проблемами, определяемыми сложностью программ, дальше всех продвинулась объектно-ориентированная технология, которая и получила наибольшее распространение. В настоящее время она успешно развивается по самым разным направлениям, затрагивая как анализ и проектирование программных систем, так и написание самих программ. Послед- нее определяется как объектно-ориентированное программирование и связано с использованием соответствующих объектно-ориентированных языков. В качестве примера языков, поддерживающих объектно-ориентированный стиль программи- рования, можно привести С++, Object Pascal, Smalltalk, Ada, Eiffel. Развитие ООП практически вытеснило процедурное программирование из раз- работки сложных программных систем.

 


Дата добавления: 2021-03-18; просмотров: 108; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!