Над разработкой очень больших программных проектов трудятся команды программистов.
Для командной работы нужны подробный план, чёткая организация и полное взаимопонимание.
К программированию нужен системный подход.
Применение технологий программирования (software engineering) позволяет облегчить разработку компьютерных программ.
Термин — «решение задачи» (solving problem) — охватывает все этапы: от постановки задачи до разработки компьютерной программы для её рещеия.
Данный процесс состоит из нескольких этапов:
Алгоритм (algorithm) — пошаговое описание метода решения задачи за конечный отрезок времени.
Алгоритмы часто работают со структурами данных.
Например, алгоритм может вносить новые данные в структуру, удалять их оттуда либо просматривать.
Для решения задачи нужно не просто хранить данные, но и организовывать их таким образом, чтобы ускорить выполнение алгоритма.
Большую часть внимания следует уделять способам организации данных в различных структурах.
В ходе разработки программного обеспечения следует учитывать долгий и продолжительный процесс, называемый жизненным циклом программного обеспечения (software's life cycle).
Этот процесс начинается с первоначальной идеи, включает в себя написание и отладку программ и продолжается многие годы, в течение которых в исходное программное обеспечение вносятся изменения и улучшения.
Этапы жизненного цикла программного обеспечения представляют собой части некоторого умозрительного круга, а не простого линейного списка: например, тестирование программы может предполагать внесение изменений как в постановку задачи, так и сам проект.
Все девять этапов включают документирование.
Документирование программы не является отдельным этапом её жизненного цикла, а сопровождает её на протяжении всей жизни.
Этапы жизненного цикла программного обеспечения:
Постановка задачи должна быть точной и подробной.
Будет приведён перечень вопросов, на которые следует ответить.
Для полного взаимопонимания между заказчиками и исполнителями можно написать макетные программы (proyotype programms), имитирующие поведение отдельных частей создаваемого программного обеспечения.
Например, простая, хоть даже неэффективная, программа может демонстрировать предполагаемый пользовательский интерфейс. Лучше выявить все подводные камни либо изменить подход к решению задачи на данном этапе, а не в процессе программирования или при эксплуатация программного обеспечения.
Лучше всего упростить процесс решения задачи, разбив большую задачу на несколько маленьких, которыми было бы легче управлять.
В результате программа будет состоять из нескольких модулей (modules), представляющих собой самостоятельные единицы кода.
Модуль может содержать одну или несколько функций, а также другие блоки кода.
Следует стремиться к тому, чтобы модули были как можно более независимыми или слабо связанными (loosely coupled) друг с другом.
Разумеется, это не относится к их интерфейсам (interfaces), представляющим собой механизм их взаимодействия.
Умозрительно модули можно считать изолированными друг от друга.
Каждый модуль должен выполнять свою точно определённую задачу. Следовательно, он должен быть узкоспециализированным (highly cohesive).
Модульность (modularity) — свойство программ, состоящих из слабо связанных и узко специализированных модулей.
На этапе проектирования важно точно указывать не только предназначение кжадого модуля, но и поток данных (data flow) между модулями.
Эту спецификацию можно рассматривать как контракт (contract) между вашей функцией и вызывающим её модулем.
Если вы разрабатываете программу самостоятельно, этот контракт поможет систематически разбить исходную задачу на более мелкие части.
Если над проектом работает команда программистов, контракт поможет разделить ответственность между ними.
Программист, разрабатывающий функцию сортировки, должен выполнять контракт.
Контракт законченной функции сортировки сообщает остальным программистам, как её вызывать и какие результаты она должна возвращать.
Формулируя предусловие (predcondition) и постусловие (postcondition), вы пишете её контракт из условий, что должны выполняться перед её вызовом и после завершения её работы, соответственно.
Например, псведокод сортировки, придерживающийся приведённого выше контракта, выглядит следующим образом.
sort (anArrat, n)
// Черновой набросок спецификаций — сортировка массива.
// Предусловие: переменная — «anArray» — является массивом, состоящим из n целых чисел; n > 0
Постусловие: целлые числа массиве — «anArray» — упорядочены.
sort (anArray, n)
// Сортировка массива в возрастающем порядке.
Предусловие: переменная — «anArray» — является массивом, состоящим из целых чисел с количеством — «n»; 1 <= n <= MAX_ARRAY, где значение — «MAX_ARRAY» — глобальная константа, задающая максимальный размер массива — «anArray».
Постусловие: anArray[0] <= anArray[1] <= ... <= anArray[n - 1]; число — «n» — не меняется.
Новички могут стремиться приуменьшить значение документации, особенно когда они одновременно являются и разработчиками, и программистами, и пользователями программы.
Вы разработали функцию — «anArray» — но не указали условия её контракта — вспомните ли вы о них при её реализации через неделю? Что лучше освежает память: код на языке — «C++» — или пред- и постусловия, сформулированные простым языком?
При увеличении размера программы важность документации возрастает независимо от того, в одиночку вы пишете программу или в команде.
Не следует пренебрегать возможностью применения готовых модулей, решающих вашу задачу. Возможности повторного использования кода, предоставленные языком — «C++» — обычно реализуются в виде компилируемых библиотек. Это означает, что вы не всегда будете иметь доступ к исходному коду функции. Библиотеки представляют собой яркий пример коллекции готовых компонентов программного обеспечения.
Например, вы знаете, как использовать стандартную функцию — «sqrt» — содержащуются в математической библиотеке языка — «C++» — (math.h), однако не можете увидеть её исходный текст. Если функциипередат ьчисло с плавающей точкой или соответствующее выражение, она извлечёт из него квадратный корень и вернёт его в вызывающий модуль.
Создание ПО сопряжено с риском.
Некоторые проблемы присущи всем проектам, а некоторые — характерны лишь для определённых разработок.
Кое-какие из них можно предвитеть, в то время как другие остаются в тени.
Они могут влиять на график и стоимость выполнения работ, экономические успехи и даже на жизнь и здоровье людей.
Диагностическое утверждение (assention) — формальное высказывание, описывающее конкретные условия, что должны выполняться в определённой точки программы.
Пред- и постусловия представляют собой пример простых утверждений об условиях, что должны выполняться в начале и конце функции.
Инвариант (invariant) — условие, которое всегда должно быть истинным в конкретной точке алгоритма.
Инвариант цикла (loop invariant) — условие, что должно выполняться до и после каждого выполнения цикла, являющегося частью алгоритма.
Это напоминает доказательство теоремы в дисциплине — «геометрия».
Например, чтобы доказать, что функция работает правильно, нужно начать с проверки её предусловия, аналогичного аксиомам и предположениям в представленной ранее дисциплине, и продемонстрировать, что шаги алгоритма в итоге приводят к выполнению постусловия.
Для этого нужно проверить каждый шаг алгоритма и показать, что из диагностического утверждения, относящегося к моменту времени, предшествующему выполнению конкретного шага, следует диагностическое утверждение, относящееся к моменту времени после выполнения этого шага.
Если в процессе верификации программы обнаружилась ошибка, алгоритм можно исправить, а постановку задачи можно немного изменить.
Используя инварианты, можно доказать, что ошибка содержалась не в коде, а в самом алгоритме.
В результате время, затраченное на отладку программы, сушественно сократится.
С помощью формальных методов можно доказать правильность разных конструкций, циклов и операторов присваивания.
Для проверки правильности итерационных алгоритмов широко используются инварианты циклов.
// Вычисляем сумму элементов: item[0], item[1], item[n - 1] — для любого числа n >=1.
int sum = 0;
int j = 0;
while (j < n)
{
sum += item[j];
j++;
}
После каждого шага инициализации переменных, но до начала цикла.
Перед каждым повторением цикла.
После каждого повторения цикла.
После завершения цикла.
// Вычисляем сумму элементов: item[0], item[1], item[n - 1] — для любого числа — n >=1. // После этого
int sum = 0;
int j = 0;
while (j < n)
{ // Здесь
sum += item[j];
j++; // После этого
} // После этого
1. Инвариант должен быть истинным изначально до начала первой итерации. В предыдущем примере инвариант утверждает, что значение переменной — «sum» — равно сумме элементов массива: от — «item[0]» — до — «item[j]». Это утверждение истинно, поскольку в данном диапазоне индексов элементов нет.
2. Выполнение цикла должно сохранять инвариант. Это означает, что, если перед каждой итерацией цикла инвариант является истинным, нужно показать, что он остаётся истинным и после её выполнение. В нашем примере цикл добавляет элемент — «item[j]» — после переменной — «sum» — а затем увеличивает значение переменной — «j» — на единицу
После выполнения цикла к переменной добавляется последний элемент.
После выполнения цикла инвариант остаётся истинным.
3. Из выполнения инварианта должна следовать правильность алгоритма. Нужно показать, что, если после завершения цикла инвариант остаётся истинным, алгоритм является корректным. В предыдущем примере по завершении цикла переменная — «j» — содержит значение — «n» — следовательно, инвариант цикла остаётся истинным: переменная — «sum» — содержит сумму элементов массива: от — «item[0]» — до — «item[n - 1]».
4. Цикл должен завершиться. Нужно доказать, что цикл завершится после выполнения конечного числа итераций. В нашем примере переменная — «j» — сначала равна 0, а затем при каждой итерации увеличивается на 1. В конце вонцов переменная — «j» — станет равной числу — «n» — при любом — n >= 1
Допустим, что в предыдущем примере в операторе, вместо того условия, поставлено условие — j < n;
Шаги 1 и 2 в доказательстве правильности программы остаются без имзенения, а 3 шаг изменится: по завершении цикла переменная будет содержать число — n + 1 — и, поскольку инвариант цикла должен быть истинным, переменная — «sum» — станет содержать сумму элементов массива: от — «item[0]» — до — «item[n]».
Так как при этом мы получаем неверное решение задачи, цикл следует признать неправильным.
Доказательство истинности инварианта в начальный момент называется базисом индукции (base case). Оно аналогично доказательству, что некоторое свойство выполняется для натурального числа — 0. Доказательство истинности инварианта на каждой итерации цикла называется шагом индукции (induction step). Он аналогичен доказательству утверждения, что, если некоторое свойство выполнятеся для натурального числа — «k» — оно выполняется и для числа со значением — «k + 1».
После выполнения четырёх шагов, перечисленных выше, мы приходим к выводу, что инвариант является истинным после каждой итерации цикла, точно так же, следуя принципу математической индукции, можно доказать, что некоторое свойство выполняется для любого натурального числа.
Кодирование заключается в переводе алгоритма на конкретный язык программирования с последующим исправлением синтаксических ошибок.
Именно кодирование многи считают собственно программированием.
И всё же следует понимать, что кодирование — не самое главное, а лишь один из этапов жизненного цикла ПО.
На этапе тестирования нужно выявить и исправить как можно больше логических ошибок. Для этого можно прибегнуть к проверке отдельных функций, применяя их к выбранным данным и сравнивая с заранееизвестным результатом. Если входные данные изменяются в каком-то диапазоне, обязательно проверьте их крайние значения.
Например, если входное значение — «n» — может изменяться от «1» до «10», обязательно протестируйте программу при значениях — «1», «10». Кроме того проверьте, как работает программа, если в неё ввести заведомо неверные данные, и может ли она обнаруживать такие ошибки.
Попробуйте ввест в программу случайно выбранные данные, а затем примените её для реального набора данных.
Результатом выполнения этапов 1—6 является работающая программа, что интенсивно тестировали и отлаживали.
Лучше всего решать задачу при наиболее простых предположения, постепенно усложняя программу. Например, можно предположить, что входные данные имеют определённый формат и ялвяются правильными.
Создав простейший вариант, можно дополнять его более сложными процедурами ввода и вывода данных, оснащать дополнительными возможностями и средствами для обнаружения ошибок.
Если вы применяете данный подход, этап уточнения решения становится необходимым.
Окончательное уточнение решения не должно приводить к полному пересмотру программы.
Каждое уточнение решения является довольно очевидным, особенно если программа имеет модульную структуру.
Фактически постепенное уточнение решения представляет собой основное преимущество модульного подхода к разработке программ.
Кроме того после каждой даже простейшей модификации программы её нужно снова тщательно протестировать.
После завершения разработки программного продукта он распространяется среди пользователей, инсталлируется на их компьютерах и применяется.
Поддержка программы не имеет ничего общего с обслуживанием автомобиля.
Программное обеспечение не износится, если за ним не ухаживать.
Пользователи ваши программ могут обнаружить ошибки, оставшиеся незамеченными при тестировании.
Кроме того со временем ПО нужно совершенствовать, добавляя в него новые функциональные возможности или модифицируя его компоненты.
Авторы программ могут заниматься этим довольно редко, тем важнее становится наличие документации.
Решение этой задачи имеет реальную и вполне ощутимую стоимость.
В неё входят ресурсы компьютера (время вычислений и память), потреблённые программой, неудобства, с которыми сталкиваются пользоавтели программы, и последствия, к которым приводит её непправильная работа. Эти факторы относятся лишь к одному из этапов жизненного цикла программы — этапу её поддержки.
Общая стоимость решения должна учитывать объём рабочего времени, затраченного программистами, что его разрабатывали, уточняли, кодировали, отлаживали и тестировали.
Кроме того необходимо учесть стоимость поддержки, модификации и усовершенствования программы.
Решение считается нормальным, если его общая стоимость минимальна. Интересно проследить, как изменялась относительная важность разных компонентов в ходе эволюции программирования.
Вначале доля стоимости работы компьютера по сравнению со стоимостью работы программистов была чрезвычайно высока. Кроме того программиста разрабатывались для решения очень специфичных, узко поставленных задач.
Если постановка задачи изменялась, создавалась новая программа, поддержка программ в овнимание не принималась, их читабельность не имела никакого значения, поэтому программу обычно использовал только дин человек, её автор.
Как следствие, программистов не интересовало, удобно ли работать с программой. Интерфейс программы не считался важным фактором.