Жизненный цикл программного обеспечения

Решение задач и разработка программного обеспечения

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

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

К программированию нужен системный подход.

Применение технологий программирования (software engineering) позволяет облегчить разработку компьютерных программ.

Решение задачи

Термин — «решение задачи» (solving problem) — охватывает все этапы: от постановки задачи до разработки компьютерной программы для её рещеия.

Данный процесс состоит из нескольких этапов:

Решение состоит из алгоритмов и способов хранения данных

Алгоритм (algorithm) — пошаговое описание метода решения задачи за конечный отрезок времени.

Алгоритмы часто работают со структурами данных.

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

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

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

Жизненный цикл программного обеспечения

В ходе разработки программного обеспечения следует учитывать долгий и продолжительный процесс, называемый жизненным циклом программного обеспечения (software's life cycle).

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

Девять этапов жизненного цикла программного обеспечения

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

Все девять этапов включают документирование.

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

Этапы жизненного цикла программного обеспечения:

Этап 1: постановка задачи

Постановка задачи должна быть точной и подробной.

Будет приведён перечень вопросов, на которые следует ответить.

Макетные программы позволяют прояснить постановку задачи

Для полного взаимопонимания между заказчиками и исполнителями можно написать макетные программы (proyotype programms), имитирующие поведение отдельных частей создаваемого программного обеспечения.

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

Этап 2: разработка

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

В результате программа будет состоять из нескольких модулей (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), однако не можете увидеть её исходный текст. Если функциипередат ьчисло с плавающей точкой или соответствующее выражение, она извлечёт из него квадратный корень и вернёт его в вызывающий модуль.

Этап 3: оценка риска

Создание ПО сопряжено с риском.

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

Кое-какие из них можно предвитеть, в то время как другие остаются в тени.

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

Этап 4: верификация

Диагностическое утверждение (assention) — формальное высказывание, описывающее конкретные условия, что должны выполняться в определённой точки программы.

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

Инвариант (invariant) — условие, которое всегда должно быть истинным в конкретной точке алгоритма.

Инвариант цикла (loop invariant) — условие, что должно выполняться до и после каждого выполнения цикла, являющегося частью алгоритма.

Доказательство правильности алгоритма

Это напоминает доказательство теоремы в дисциплине — «геометрия».

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

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

Что нужно делать при возникновении ошибки

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

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

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

Что можно сделать с помощью формальных методов

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

Для проверки правильности итерационных алгоритмов широко используются инварианты циклов.

Цикл вычисляет сумму первых n элементов массива

// Вычисляем сумму элементов: 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]».

Так как при этом мы получаем неверное решение задачи, цикл следует признать неправильным.

Математическая индкукция (mathematical induction)

Доказательство истинности инварианта в начальный момент называется базисом индукции (base case). Оно аналогично доказательству, что некоторое свойство выполняется для натурального числа — 0. Доказательство истинности инварианта на каждой итерации цикла называется шагом индукции (induction step). Он аналогичен доказательству утверждения, что, если некоторое свойство выполнятеся для натурального числа — «k» — оно выполняется и для числа со значением — «k + 1».

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

Этап 5: кодирование

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

Именно кодирование многи считают собственно программированием.

И всё же следует понимать, что кодирование — не самое главное, а лишь один из этапов жизненного цикла ПО.

Этап 6: тестирование

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

Например, если входное значение — «n» — может изменяться от «1» до «10», обязательно протестируйте программу при значениях — «1», «10». Кроме того проверьте, как работает программа, если в неё ввести заведомо неверные данные, и может ли она обнаруживать такие ошибки.

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

Этап 7: уточнение решения

Результатом выполнения этапов 1—6 является работающая программа, что интенсивно тестировали и отлаживали.

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

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

Подход — «от простого к сложному»

Если вы применяете данный подход, этап уточнения решения становится необходимым.

Окончательное уточнение решения не должно приводить к полному пересмотру программы.

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

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

Кроме того после каждой даже простейшей модификации программы её нужно снова тщательно протестировать.

Этап 8: производство

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

Этап 9: сопровождение

Поддержка программы не имеет ничего общего с обслуживанием автомобиля.

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

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

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

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

Программа создаётся для решения конкретной задачи

Решение этой задачи имеет реальную и вполне ощутимую стоимость.

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

Общая стоимость решения должна учитывать объём рабочего времени, затраченного программистами, что его разрабатывали, уточняли, кодировали, отлаживали и тестировали.

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

Многомерная точка зрения на стоимость решения

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

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

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

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