Перейти к содержанию

Лекция 2. Методы программирования и жизненный цикл ПО (Programming Methods and SDLC)

Основными технологиями разработки программного обеспечения являются:

  • Императивное программирование
  • Структурное программирование
  • Модульное программирование
  • Объектно-ориентированное программирование

Императивное программирование

Императивное программирование — это исторически первая методология программирования, которой пользовался каждый, кто писал на любом из «массовых» языков (Basic, Pascal, C).

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

Методы и концепции

  • Метод изменения состояний — последовательное изменение состояний. Поддерживается концепцией алгоритма.
  • Метод управления потоком исполнения — пошаговый контроль управления. Поддерживается концепцией потока исполнения.

Вычислительная модель. Если под вычислителем понимать современный компьютер, то его состоянием будут значения всех ячеек памяти, состояние процессора (в том числе указатель текущей команды) и всех сопряжённых устройств. Единственная структура данных — последовательность ячеек («адрес» → «значение») с линейно упорядоченными адресами.

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

Синтаксис и семантика

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

  • простые операторы — никакая их часть не является самостоятельным оператором (присваивание, безусловный переход, вызов процедуры и т. п.);
  • структурные операторы — объединяют другие операторы в новый, более крупный (составной оператор, операторы выбора, цикла и т. п.).

Традиционное средство структурирования — подпрограмма (процедура или функция). Подпрограммы имеют параметры и локальные определения, могут быть вызваны рекурсивно. Функции возвращают значения как результат своей работы.

Класс задач

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

Структурное программирование

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

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

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

Методология структурного императивного программирования заключается в задании хорошей топологии императивных программ — в том числе:

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

Подход базируется на двух основных принципах:

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

Создателем структурного подхода считается Эдсгер Дейкстра. В разработке участвовали Х. Милс, Д. Э. Кнут, Ч. Хоар.

Методы и концепции

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

Теорема о структурировании (Бёма — Якопини)

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

  • последовательность;
  • выбор;
  • повторение (цикл).

Следствия:

  1. Всякую программу можно привести к форме без оператора goto.
  2. Любой алгоритм можно реализовать в языке, основанном на трёх управляющих конструкциях.
  3. Сложность структурированных программ ограничена даже в случае их неограниченного размера.

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

  • в Python: break, continue, return, исключения;
  • в Go: break, continue, помеченные break/continue для выхода из вложенных циклов, ранний return, defer/panic/recover для обработки ошибок.

В Go формально есть goto — но его использование в идиоматическом коде минимально (обычно — для прыжков в генерируемом коде).

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

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

Концепции модульного программирования

  1. Принцип утаивания информации Парнаса. Всякий компонент утаивает единственное проектное решение. Сначала формируется список проектных решений, которые трудно принять или которые, скорее всего, будут меняться. Затем определяются отдельные модули, каждый из которых реализует одно из решений.
  2. Аксиома модульности Коуэна. Модуль — независимая программная единица, служащая для выполнения определённой функции программы. Должен удовлетворять условиям:
    • блочность организации — возможность вызвать модуль из блоков любой степени вложенности;
    • синтаксическая обособленность — выделение модуля в тексте синтаксическими элементами;
    • семантическая независимость — независимость от места, где модуль вызван;
    • общность данных — наличие собственных данных, сохраняющихся при каждом обращении;
    • полнота определения — самостоятельность программной единицы.
  3. Сборочное программирование Цейтина. Модули — «программные кирпичи», из которых строится программа. Предпосылки:
    • стремление к выделению независимой единицы программного знания;
    • потребность организационного расчленения крупных разработок;
    • возможность параллельного исполнения модулей.

Разновидности модулей

  1. «Маленькие» (функциональные) модули — реализуют одну определённую функцию. Простейший пример — процедура или функция.
  2. «Средние» (информационные) модули — реализуют несколько операций над одной и той же структурой данных (информационным объектом), считающейся неизвестной вне этого модуля. Примеры: классы в Python, Java, C++, структуры с методами в Go.
  3. «Большие» (логические) модули — объединяют набор средних или маленьких модулей. Примеры: пакеты в Python (package/__init__.py) и в Go.

Характеристики модуля (по Майерсу)

  1. Размер модуля. В модуле должно быть 7 ± 2 конструкций (это число берётся на основе представлений психологов о среднем оперативном буфере памяти человека). Модуль (функция) не должен превышать 60 строк — чтобы его можно было поместить на одну страницу распечатки или легко просмотреть на экране.
  2. Прочность (связность) модуля. Существует гипотеза о глобальных данных: глобальные данные вредны и опасны. Связность — мера независимости частей модуля. Чем выше связность — тем лучше. Типы связности (по возрастанию качества):
    • функциональная — модуль реализует одну определённую функцию и не может быть разбит;
    • последовательная — может быть разбит на последовательные части, выполняющие независимые функции, но совместно реализующие единственную;
    • информационная (коммуникативная) — модуль выполняет несколько операций над одной и той же структурой данных (применяется для реализации абстрактных типов данных).
  3. Сцепление модуля с другими. Сцепление — мера относительной независимости модуля. Независимые модули могут быть модифицированы без переделки других. Чем слабее сцепление — тем лучше.
    • Независимые модули — идеал, но достижим редко.
    • Сцепление по данным (параметрическое) — данные передаются как значения параметров или как результат вызова. Хороший вариант, реализуется в большинстве языков.
  4. Рутинность (идемпотентность) — независимость модуля от предыдущих обращений. Модуль рутинен, если результат зависит только от переданных параметров (а не от количества обращений). В большинстве случаев модуль должен быть рутинным.

Объектно-ориентированное программирование (ООП)

Метод структурного программирования эффективен при написании программ ограниченной сложности. С возрастанием сложности проектов возможности СП оказались недостаточными — в программе не отражалась непосредственно структура явлений и понятий реального мира.

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

Теоретические основы ООП были заложены ещё в 70-х годах прошлого века, но практическое воплощение стало возможно лишь в середине 80-х.

Методология ООП использует метод объектной декомпозиции: структура системы (статическая составляющая) описывается в терминах объектов и связей между ними, а поведение системы (динамическая составляющая) — в терминах обмена сообщениями между объектами. Сообщения могут быть как реакцией на события, вызываемые внешними факторами, так и порождаемые самими объектами.

Основные концепции

  • Объект имеет набор обработчиков сообщений (методов) и поля — персональные переменные данного объекта. Сам объект относится к некоторому классу.
  • Класс в объектно-ориентированных языках описывает структуру и функционирование множества объектов с подобными характеристиками, атрибутами и поведением.
  • Методы — функциональные свойства, которые можно активизировать.

В ООП выделяют три основных свойства:

  • Инкапсуляция — сокрытие информации и комбинирование данных и функций (методов) внутри объекта.
  • Наследование — построение иерархии порождённых объектов с возможностью для каждого наследника доступа к коду и данным предков.
  • Полиморфизм — присваивание действию одного имени, разделяемого по иерархии объектов, причём каждый объект иерархии выполняет действие способом, подходящим именно ему.

Подробно ООП и его принципы разбираются в Теме 6 и Теме 9.

Классификация ООП-языков

  • Чистые — поддерживают ООП в наиболее классическом виде (Smalltalk, Ruby).
  • Гибридные — появились в результате внедрения объектно-ориентированных конструкций в популярные императивные языки (C++, Python — частично, OCaml).
  • Урезанные — без некоторых «опасных» возможностей (Java, C#, Go в части ООП — нет наследования, только композиция через встраивание и интерфейсы).

Особенность Go. В Go нет классов и наследования в классическом понимании. Полиморфизм достигается через интерфейсы — типы, описывающие набор методов. Любой тип, реализующий все методы интерфейса, неявно удовлетворяет ему. Композиция через встраивание (struct embedding) заменяет наследование. Этот выбор сделан сознательно — для упрощения языка и снижения сложности больших систем.

Общие принципы разработки ПО

Существуют общие принципы, которые следует использовать при разработке любого ПО.

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

Общесистемные принципы

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

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

Жизненный цикл (SDLC) ПО включает все этапы его развития — от возникновения потребности в нём до полного прекращения использования вследствие морального устаревания.

По длительности жизненного цикла ПО можно разделить на:

  • С малой длительностью эксплуатации — создаются для решения научных и инженерных задач (вычисление конкретных результатов). Невелики, разрабатываются одним специалистом или маленькой группой. Не предназначены для тиражирования. Сопровождение и модификация не обязательны. Жизненный цикл — редко больше 3 лет.
  • С большой длительностью эксплуатации — для регулярной обработки информации и управления. Сложная структура, тиражируются и сопровождаются документацией. Проектируются и эксплуатируются большими коллективами. Жизненный цикл — десятки лет; до 90 % этого времени приходится на эксплуатацию и сопровождение.

Обобщённая модель SDLC

  1. Системный анализ:
    • исследования;
    • анализ осуществимости — эксплуатационной, экономической, коммерческой.
  2. Проектирование ПО:
    • конструирование (функциональная декомпозиция, архитектура, внешнее проектирование, БД);
    • программирование (внутреннее проектирование модулей, кодирование, отладка, компоновка);
    • отладка ПО.
  3. Оценка (испытания) ПО.
  4. Использование ПО:
    • эксплуатация;
    • сопровождение.

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

Проектирование ПО — основная и решающая фаза жизненного цикла, во время которой ПО на 90 % приобретает свою окончательную форму. Делится на три этапа:

  • конструирование — функциональная декомпозиция, внешнее проектирование (UX), проектирование БД, архитектура;
  • программирование — внутреннее конструирование, разработка логики каждого модуля, выраженная текстом конкретной программы;
  • отладка — после того как компоненты будут отлажены по отдельности и собраны в единый программный продукт.

Оценка (испытания) ПО. ПО подвергается строгому системному испытанию со стороны группы лиц, не являющихся разработчиками. Цель — гарантировать, что готовое изделие удовлетворяет всем требованиям и спецификациям, может быть использовано в среде пользователя, свободно от дефектов и содержит необходимую документацию.

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

Типы приложений

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

Консольные приложения

Ввод/вывод информации производится при помощи стандартных потоков:

  • stdin — стандартный поток ввода;
  • stdout — стандартный поток вывода;
  • stderr — поток ошибок.

Стандартные потоки открываются автоматически при запуске программы и связаны по умолчанию с монитором. Вывод может быть перенаправлен в файл (или из файла) средствами ОС: >, >>, <, |.

Взаимодействие сводится к передаче параметров командной строки или интерактивно через stdin и выдаче программой текстовой/символьной информации через stdout или stderr.

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

Оконные приложения

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

Историческая справка. Xerox PARC → Apple (Lisa, Macintosh) → Microsoft Windows; параллельно UNIX (X Window System).

В настоящий момент используются два основных типа графических ОС:

  • клиент-серверная (X Window) — приложение шлёт запрос серверу нарисовать что-то в определённой области;
  • графическое ядро (Windows, macOS) — программа взаимодействует с ОС через системные API, являясь её клиентом.

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

Среди фреймворков и библиотек:

  • Qt (кьют) — кроссплатформенный фреймворк, изначально для C++; есть привязки ко многим языкам, для Python — PyQt/PySide.
  • .NET (WPF, WinForms; кроссплатформенный вариант — MAUI/Avalonia).
  • Electron — фреймворк для построения настольных приложений на JavaScript (используется в VS Code, Slack, Discord).
  • Tauri — современная альтернатива Electron на Rust.
  • Fyne — кроссплатформенный GUI на Go.

Другая классификация — по принципу построения

  • Standalone-приложение — программное обеспечение, которое не нуждается в дополнительных программах и зависимостях для установки и функционирования.
  • Клиент-серверное приложение — состоит из двух и более частей: тонкий клиент у пользователя, а вся бизнес-логика — на отдельном сервере.
  • Web-приложение — частный случай клиент-серверного, в котором клиентской части нет, вместо неё — браузер.

Консольные приложения: примеры

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

name = input("Как Вас зовут? ")
print("Привет,", name)

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

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    fmt.Print("Как Вас зовут? ")
    reader := bufio.NewReader(os.Stdin)
    name, _ := reader.ReadString('\n')
    name = strings.TrimSpace(name)
    fmt.Println("Привет,", name)
}

Все приложения в Go по умолчанию — консольные. Для GUI/WEB используют отдельные пакеты/фреймворки.


Контрольные вопросы

  • Перечислите и кратко охарактеризуйте методы программирования.
  • Назовите общие принципы разработки ПО.
  • Опишите стадии жизненного цикла ПО.
  • В чём разница между «короткими» и «долгоживущими» программными изделиями?
  • Какие бывают типы приложений по способу взаимодействия с пользователем?
  • В чём отличие модели потоков stdin/stdout/stderr от событийной модели графических приложений?