Опубликован: 27.07.2006 | Доступ: свободный | Студентов: 4427 / 1122 | Оценка: 4.28 / 4.13 | Длительность: 26:56:00
Специальности: Программист
Лекция 9:

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

Парадигмы программирования

По одной из классификаций языки программирования делятся на

  • директивные (directive), называемые также процедурными (procedural) или императивными (imperative),
  • декларативные (declarative) языки,
  • объектно-ориентированные (object-oriented).

К директивным языкам относятся такие классические языки программирования, как Algol, Fortran, Basic, Pascal, C. Наиболее существенными классами декларативных языков являются функциональные (functional) или аппликативные, и логические (logic) языки. К категории функциональных языков относятся, например, Lisp и Haskell. Самым известным языком логического программирования является Prolog (Пролог). Среди объектно-ориентированных языков программирования (языков ООП) отметим C++, Java, Python и Ruby.

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

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

Директивная программа - это список команд примерно такого рода: от пункта А по ул. Садовой на север до площади Славы, оттуда по ул. Пушкина два квартала, потом повернуть направо и идти до Театрального переулка, по этому переулку налево по правой стороне до дома 20, который и есть пункт Б.

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

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

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

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

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

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

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

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

Ниже приводится пример программы на языке C, в которой кроме главной функции main используются еще две подпрограммы - print_array, печатающая элементы переданного ей массива целых чисел, и selection, сортирующая массив, переданный ей в качестве аргумента.

#include <stdio.h>

void print_array(int c[], int n, char* t) {
  int i;
  printf("%s",t);
  for (i = 0; i < n; i += 1)
    printf("ta[%d]=%d", i, c[i]);
  printf("\n");
}

void selection(int c[], int n) {
    int i, j, k, x;
    
    for (i = 0; i < n; i += 1) {
	for (x = c[k=i], j = i + 1; j < n; j++)
	    if (c[j] < x) x = c[k=j];
	c[k] = c[i]; c[i] = x;
    }
}

int main(void) { 
  int a[] = {8, 3, 2, 7, 9, 5};

  int n = sizeof(a)/sizeof(int);
  print_array(a, n, "Исходный массив\n");
  selection(a, n);
  print_array(a, n, "Отсортированный массив\n");
  return 0;
}

Разместите текст этой программы в файле с именем sort.c и выполните следующие команды, компилирующие и запускающие ее:

cc sort.c
./a.out

Функция main дважды вызывает процедуру print_array: сначала для печати исходного массива, а затем, после вызова функции selection, для печати его же в уже отсортированном виде. Однажды реализованные функции print_array и selection могут быть использованы при написании относительно большой программы многократно.


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

Со временем при проектировании программ акцент сместился с организации процедур на организацию структур данных. Современные директивные языки программирования предлагают еще один метод структурирования программ: инкапсуляция (от слова capsule - капсула, контейнер) данных и подпрограмм в более крупные объекты, называемые модулями. Большую часть данных модуля и выполняемые операторы можно скрыть таким образом, что их нельзя будет изменить или использовать способами, отличными от заранее предопределенных. Эта парадигма известна, как принцип сокрытия данных. Если в языке нет возможности сгруппировать процедуры вместе с данными, то он плохо поддерживает модульный стиль программирования.

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

Стек функционирует точно также, только в нем хранится совокупность произвольных элементов. Новый элемент помещается на вершину стека с помощью операции втолкнуть (push). Виден в стеке только самый верхний элемент, который может быть извлечен из него командой вытолкнуть (pop). Иногда говорят, что стек задает дисциплину обслуживания LIFO (Last In First Out - последним пришел, первым выйдешь). Организация данных в виде стека широко распространена в программировании. Например, управление автоматически распределяемой памятью в процессе выполнения программы производится по принципу стека.

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

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

Дмитрий Фаттахов
Дмитрий Фаттахов
Виктория Бельгесова
Виктория Бельгесова

Добрый день. Как получить удостоверение о прохождении данного курса?