Особенности модульного программирования
Модульное программирование
Модуль – это последовательность логически связанных фрагментов, оформленных как отдельная часть программы.
К модулю предъявляются следующие требования:
1) модуль должен реализовывать единственную функцию, т.е. при построении модуля используется концепция: «один модуль – одна функция». Таким образом, модуль – это элемент программы, выполняющий самостоятельную задачу. На его входе он может получать определенный набор исходных данных, обрабатывать их в соответствии с заданным алгоритмом и возвращать результат обработки, т.е. реализуется стандартный принцип IPO (Input – Process – Output) – вход-процесс-выход;
2) на модуль нужно ссылаться с помощью его имени. Он должен иметь один вход и один выход, что гарантирует замкнутость модуля и упрощает сопровождение программ;
3) модуль должен иметь функциональную завершенность, т.е. выполнять перечень регламентированных операций для реализации каждой отдельной функции в полном составе, достаточных для завершения начатой обработки;
4) модуль должен возвращать управление в точку его вызова, в свою очередь, он должен иметь возможность сам вызывать другие модули;
5) модуль не должен сохранять историю своих вызовов и использовать ее при своем функционировании;
6) модуль должен иметь логическую независимость, т.е. результат работы программного модуля зависит только от исходных данных, но не зависит от работы других модулей;
7) модуль должен иметь слабые информационные связи с другими программными модулями – обмен информацией между модулями должен быть по возможности минимизирован;
8) модуль должен быть сравнительно невелик, т.е. быть обозримым по размеру и сложности. Опытные программисты рекомендуют его размер не более двух страниц распечатки на принтере.
Для достижения независимости модулей часто используется принцип информационной локализованности, который состоит в том, что вся информация о структуре данных, о прототипах функций, констант и т.д. сосредотачивается («упрятывается») в отдельном модуле. Доступ к этой информации осуществляется только через этот модуль (в алгоритмическом языке С/С++ такие модули имеют расширение *.h).
Программирование с использованием модулей называется модульным программированием. Оно возникло еще в начале 60-х годов XX в. Модульное программирование основано на идее использования уровней абстракции, когда вся проблема или комплекс задач разбивается на задачи, подзадачи, абстрагируется и представляется в виде иерархического дерева связанных между собой модулей, в совокупности представляющих создаваемое программное обеспечение (ПО).
Достоинствами модульного программирования является следующее:
· большую программу могут писать одновременно несколько программистов, что позволяет раньше закончить задачу;
· можно создавать библиотеки наиболее употребительных модулей;
· упрощается процедура загрузки в оперативную память большой программы, требующей сегментации;
· появляется много естественных контрольных точек для отладки проекта;
· проще проектировать и в дальнейшем модифицировать программы.
Недостатки модульного программирования заключаеются в следующем:
· возрастает размер требуемой оперативной памяти;
· увеличивается время компиляции и загрузки;
· увеличивается время выполнения программы;
· довольно сложными становятся межмодульные интерфейсы.
Модульное программирование реализуется через модули – функции. Функция – это область памяти, выделяемая для сохранения программного кода, предназначенного для выполнения конкретной задачи. Другими словами, функция – минимальный исполняемый модуль программы на языке С/С++. По умолчанию функция имеет тип external, и доступ к ней возможен из любого файла программы. Но она может быть ограничена спецификатором класса памяти static.
Функция характеризуется типом, областью действия связанного с функцией имени, видимостью имени функции, типом связывания.
Все функции имеют рекомендуемый стандартами языка единый формат определения. Он имеет заголовок функции, в котором задаются: тип, имя функции и спецификация формальных параметров:
Тип имя_функции (спецификация_параметров) тело_функции
Тип – это тип возвращаемого функцией значения, в том числе void (кроме типов массива или функции). Умолчанием является тип int. Если тип возврата функции не void, то тело функции должно содержать как минимум один оператор return.
Имя_функции – идентификатор, с помощью которого можно обратиться к функции. Он выбирается программистом произвольно и не должен совпадать со служебными словами и с именами других объектов программы. Однако любая программа на языке С/С++ должна иметь хотя бы одну функцию с именем main – главную функцию, содержащую точку входа в программу.
Спецификация_параметров – список формальных параметров, т.е. переменных, принимающих значения, передаваемые функции при ее вызове. Список формальных параметров перечисляется через запятую. Каждый формальный параметр должен иметь следующий формат:
Тип может быть встроенным (int, long, float, double и т.д.), структурой (struct), объединением (union), перечислением (enum), указателями на них или на функции или классы (class). Имя_формального_параметра представляет собой имя используемой в теле функции переменной. Идентификаторы формальных параметров не могут совпадать с именами локальных переменных, объявленных внутри тела функции.
Объявление формального параметра может содержать инициализатор, то есть выражение, которое должно обеспечить параметру присвоение начального значения. Инициализатор параметра не является константным выражением. Начальная инициализация параметров происходит не на стадии компиляции (как, например, выделение памяти под массивы), а непосредственно в ходе выполнения программы.
В языке С/C++ допустимы функции, количество параметров у которых при компиляции функции не фиксировано, следовательно, остаются неизвестными и их типы. Количество и типы параметров таких функций становятся известными только при их вызове, когда явно задан список фактических параметров. При определении и описании таких функций со списками параметров неопределенной длины спецификацию формальных параметров следует закончить запятой и многоточием.
Каждая функция с переменным количеством параметров должна иметь хотя бы один обязательный параметр. После списка обязательных параметров ставится запятая, а затем многоточие, извещающее компилятор, что дальнейший контроль соответствия количества и типов параметров при обработке вызова функции проводить не нужно.
Спецификация_параметровможет отсутствовать, то есть скобки могут быть пустыми, но в этом случае рекомендуется указывать тип void.
Тело_функции – часть определения функции, ограниченная фигурными скобками и непосредственно размещенная вслед за заголовком функции. Тело_функцииможет быть либо составным оператором, либо блоком. Например:
Теоретический материал. Модульное программирование;
Модульное программирование
Лекция №10
Вопросы для контроля
- Перечислите известные Вам методы проектирования программ
- Какие методы программирования Вам известны? В чем их суть?
- В чем заключается метод декларативного и логического проектирования?
- Охарактеризуйте метод логического и метод функционального программирования
- Чем отличаются метод структурного проектирования и метод создания проекта?
Обучающая: получить представление о модульном программировании, о понятии «модуль», его характеристиках, видах модулей, о методах разработки, применяемых при модульном программировании;
Ведущий метод обучения: объяснительно-иллюстративный.
Оснащение занятия: конспект лекции, презентации.
Для обеспечения технологичности разрабатываемого программного обеспечения применяется модульное программирование.
1. Понятие модуля
Приступая к разработке программы, следует иметь ввиду, что она, как правило, является большой системой, поэтому необходимо принять меры для ее упрощения. Для этого программу разрабатывают по частям, которые называются программными модулями. Такой метод создания программ называется модульным программированием.
Модульное программирование основано на понятии модуля – программы или функционально завершенного фрагмента программы.
• один вход и один выход. На входе программный модуль получает определенный набор исходных данных, выполняет их обработку и возвращает один набор выходных данных;
• функциональная завершенность. Модуль выполняет набор определенных операций для реализации каждой отдельной функции, достаточных для завершения начатой обработки данных;
• логическая независимость. Результат работы данного фрагмента программы не зависит от работы других модулей;
• слабые информационные связи с другими программными модулями. Обмен информацией между отдельными модулями должен быть минимален;
Таким образом, модули содержат описание исходных данных, операции обработки данных и структуры взаимосвязи с другими модулями.
Программный модуль является самостоятельным программным продуктом. Это означает, что каждый программный модуль разрабатывается, компилируется и отлаживается отдельно от других модулей программы. Более того, каждый разработанный программный модуль может включаться в состав разных программных систем при условии выполнения требований, предъявляемых к его использованию в документации к этому модулю. Таким образом, программный модуль может рассматриваться и как средство упрощения сложных программ, и как средство накопления и многократного использования программистских знаний.
2. Основные характеристики программного модуля
В литературе приводятся различные критерии оценки приемлемости модуля. Были предложены следующие критерии:
• хороший модуль снаружи проще, чем внутри;
• хороший модуль проще использовать, чем построить.
Предлагается использовать следующие характеристики программного модуля для оценки его приемлемости: размер модуля, прочность модуля, сцепление с другими модулями и рутинность модуля.
Размер модуля измеряется числом содержащихся в нем операторов. Модуль не должен быть слишком маленьким или слишком большим. Большие модули, как правило, сложны для понимания и неудобны для внесения изменений, они могут существенно увеличить суммарное время повторных трансляций программы при отладке. Маленькие модули усложняют общую структурную схему программы и могут не окупать накладных расходов, связанных с их оформлением. Обычно рекомендуются программные модули размером от нескольких десятков до нескольких сотен операторов.
Прочность модуля — это мера его внутренних связей. Чем выше прочность модуля, тем больше связей скрыто от внешней по отношению к нему части программы и, следовательно, тем проще сама программа. Самой слабой степенью прочности обладает модуль, прочный по совпадению. В данном случае в программный модуль оформляется повторяющаяся в нескольких местах программы последовательность операторов. Если вдруг возникнет необходимость изменения этой последовательности в одном из контекстов, придется изменять сам модуль, что может сделать его использование в других контекстах ошибочным. Такой класс программных модулей не рекомендуется для использования.
Функционально прочный модуль — это модуль, реализующий одну какую-либо определенную функцию. При этом он может использовать и другие модули. Такой вид прочности модулей рекомендуется для использования.
Высшей степенью прочности обладает информационно прочный модуль — это модуль, выполняющий несколько операций над одной и той же структурой данных, которая неизвестна вне этого модуля. Для каждой из этих операций в таком модуле имеется свой вход со своей формой обращения к нему. Информационно прочный модуль может реализовывать, например абстрактный тип данных.
Сцепление модуля — это мера его зависимости по способу передачи данных от других модулей. Чем слабее сцепление модуля с другими модулями, тем сильнее его независимость от других модулей. Для оценки степени сцепления существует шесть видов сцепления модулей по:
• общей области данных;
Худшим видом сцепления модулей является сцепление по содержимому. Таким является сцепление двух модулей, когда один из них имеет прямые ссылки на содержимое другого модуля (например, на константу, содержащуюся в другом модуле). Такое сцепление модулей недопустимо.
Не рекомендуется использовать также сцепление по общей области — это такое сцепление модулей, когда несколько модулей используют одну и ту же область памяти.
Сцепление по образцу предполагает, что модули обмениваются данными, объединенными в структуры. Этот тип обеспечивает неплохие характеристики по сравнению с предыдущими. Недостаток заключается в том, что конкретные передаваемые данные «спрятаны» в структуры, и потому уменьшается «прозрачность» связи между модулями. Кроме того, при изменении структуры передаваемых данных необходимо модифицировать все использующие ее модули.
При сцеплении по управлению один модуль посылает другому некоторый информационный объект (флаг), предназначенный для управления внутренней логикой модуля. Таким способом часто выполняют настройку режимов работы программного обеспечения. Подобные настройки также снижают наглядность взаимодействия модулей и потому обеспечивают не лучшие характеристики технологичности разрабатываемого программного обеспечения.
Сцепление по внешним ссылкам предполагает, что модули ссылаются на один и тот же глобальный элемент данных.
Единственным видом сцепления модулей, который рекомендуется для использования современной технологией программирования, является сцепление по данным (параметрическое сцепление) — это случай, когда данные передаются модулю либо при обращении к нему как значения его параметров, либо как результат его обращения к другому модулю для вычисления некоторой функции. Такой вид сцепления модулей реализуется на языках программирования при использовании обращений к процедурам (функциям).
Рутинность модуля — это его независимость от предыстории обращений к нему. Модуль будем называть рутинным, если результат обращения к нему зависит только от значений его параметров и не зависит от результатов предыдущих обращений к нему.Модуль будем называть зависящим от предыстории, если результат обращения к нему зависит от внутреннего состояния этого модуля, хранящего следы предыдущих обращений к нему. В книге Майерса не рекомендуется использовать зависящие от предыстории модули, так как они провоцируют появление в программах неуловимых ошибок. Однако во многих случаях именно зависящий от предыстории модуль является наиболее информационно прочным. Поэтому более приемлема следующая рекомендация:
• всегда следует использовать рутинный модуль, если это не приводит к плохим сцеплениям модулей;
• зависящие от предыстории модули следует использовать только в случае, когда это необходимо для обеспечения параметрического сцепления;
• в спецификации зависящего от предыстории модуля должна быть четко сформулирована эта зависимость таким образом, чтобы было возможно прогнозировать поведение данного модуля при разных последующих обращениях к нему.
Связность модулей — мера прочности соединения функциональных и информационных объектов внутри одного модуля. Размещение сильно связанных элементов в одном модуле уменьшает межмодульные связи, в то время как помещение сильно связанных элементов в разные модули не только усиливает межмодульные связи, но и усложняет понимание их взаимодействия. Объединение слабо связанных элементов также уменьшает технологичность модулей, делая их сложнее для понимания.
Различают следующие виды связности (в порядке убывания уровня):
При функциональной связности модуль предназначен для выполнения одной функции. Его исходные данные и операции предназначены для решения одной конкретной задачи. Такой модуль имеет максимальную связность и, как следствие, хорошую технологичность (простота компиляции, тестирования, сопровождения).
При последовательной связности модуля результат обработки данных одной функцией служит исходными данными для другой функции. Такой модуль реализует одну подпрограмму, выполняющую две функции. Модуль с последовательной связностью функций можно разбить на два модуля или более, как с последовательной, так и с функциональной связностью. При этом данные, используемые последовательными функциями, также связаны последовательно. Такой модуль выполняет несколько функций, и, следовательно, его технологичность хуже с точки зрения понимания и тестирования.
Информационно связанными считают функции, обрабатывающие одни и те же данные. Информационно связанный модуль имеет неплохие показатели технологичности, так как все функции, работающие с одними и теми же данными, собраны в один модуль, что позволяет при изменении формата данных корректировать только его. Данные, которые обрабатываются одной функцией, также считают информационно связанными.
Процедурно связаны функции или данные, которые являются частями одного процесса. При процедурной связности отдельные элементы модуля связаны крайне слабо, так как реализуемые ими операции связаны лишь общим процессом, следовательно, технологичность такого модуля хуже, чем у предыдущих.
Временная связность функций подразумевает, что эти функции выполняются параллельно или в течение некоторого периода времени. Временная связность данных означает, что они используются в некотором временном интервале. Отличительной особенностью временной связности является то, что действия, реализуемые такими функциями, обычно могут выполняться в любом порядке. Например, временную связность имеют функции, выполняемые при инициализации некоторого процесса. Большая вероятность модификации функции еще больше уменьшает показатели технологичности модулей данного вида по сравнению с предыдущими, кроме того, содержание модуля с временной связностью функций может изменяться: в него могут включаться новые действия и/или исключаться старые.
Логическая связь строится на основе объединения данных или функций в одну логическую группу, например, логически связаны компоненты модуля, содержащего функции обработки текстовой информации или данные одного и того же типа. При выполнении модуля с логически связанными компонентами всегда будет вызываться одна какая-либо его часть, при этом вызывающий и вызываемый модули будут связаны по управлению. Показатели технологичности таких модулей ниже предыдущих, так как сложно понять логику их работы.
Модуль, элементы которого имеют случайную связность, имеет самые низкие показатели технологичности, так как его элементы вообще не связаны.
В табл. 2 представлены характеристики различных видов связности по экспертным оценкам
Таблица 2. Сравнительные характеристики различных видов связности
Модульное программирование
Параметры процедурного типа
Все рассмотренные параметры подпрограмм позволяли выполнять один и тот же алгоритм с различными данными. В Паскале есть и другая возможность — параметризовать алгоритм функциями и процедурами. Это может пригодиться, если требуется выполнить одну и ту же последовательность действий, внутри которой выполняется обращение к разным функциям или процедурам.
Описание параметра подпрограммы в большинстве случаев состоит из имени и типа. Имя функции является константой процедурного ( функционального ) типа, который требуется описать в разделе type , например:
Здесь вводится описание трех типов. Первый из них соответствует любой функции с одним аргументом вещественного типа, возвращающей вещественное значение, второй — процедуре без параметров, а третий — процедуре с тремя параметрами типа word . Как видно из примеров, описание процедурного (функционального) типа соответствует заголовку подпрограммы без имени. Имя типа используется затем в списке параметров подпрограммы аналогично другим типам.
Пример. Программа, вычисляющая определенные интегралы методом прямоугольников для двух функций
на интервале [a, b] с заданным количеством его разбиений ( пример 4.5).
Вычисление определенного интеграла методом прямоугольников состоит в приближенном подсчете площади, ограниченной осью абсцисс, графиком функции и границами интервала. Интервал разбивается на заданное количество промежутков, и площади получившихся фигур заменяются площадями прямоугольников.
Итак, чтобы передать имя функции или процедуры в подпрограмму, необходимо:
- Определить соответствующий процедурный тип.
- Задать для функций и процедур, предназначенных для передачи в подпрограмму, ключ компилятора <$F +>, определяющий дальнюю адресацию. При этом компилятор формирует полный адрес, состоящий из сегмента и смещения. Альтернативный способ — указать в заголовке каждой функции директиву far :
function Q(x : real) : real; far;
Рекурсивные подпрограммы
Рекурсивной называется подпрограмма, в которой содержится обращение к самой себе. Такая рекурсия называется прямой. Есть также косвенная рекурсия, когда две или более подпрограмм вызывают друг друга.
При обращении подпрограммы к самой себе происходит то же самое, что и при обращении к любой другой функции или процедуре: в стек записывается адрес возврата, резервируется место под локальные переменные, происходит передача параметров, после чего управление передается первому исполняемому оператору подпрограммы. При повторном вызове этот процесс повторяется. Для завершения вычислений каждая рекурсивная подпрограмма должна содержать хотя бы одну нерекурсивную ветвь, заканчивающуюся возвратом в вызывающую программу.
При завершении подпрограммы область ее локальных переменных освобождается, а управление передается на оператор, следующий за рекурсивным вызовом.
Простой пример рекурсивной функции — вычисление факториала (это не означает, что факториал следует вычислять именно так). Чтобы получить факториал числа n, требуется умножить на n факториал ( n – 1)!. Известно также, что 0! = 1 и 1! = 1.
Рекурсивные подпрограммы чаще всего применяют для компактной записи рекурсивных алгоритмов, а также для работы со структурами данных, описанными рекурсивно, например с двоичными деревьями. Любую рекурсивную функцию можно реализовать без применения рекурсии: для этого программист должен сам обеспечить распределение памяти под необходимое количество копий параметров.
Достоинством рекурсии является компактная запись. К недостаткам относятся расход времени и памяти на повторные вызовы функции и передачу ей параметров, а главное, опасность переполнения стека.
Модули
Модуль — это подключаемая к программе библиотека ресурсов. Он может содержать описания типов, констант, переменных и подпрограмм. В модуль обычно объединяют связанные между собой ресурсы: например, в составе оболочки есть модуль Graph для работы с экраном в графическом режиме. Модули применяются как библиотеки, которые могут использоваться различными программами, и для разбиения сложной программы на составные части.
Чтобы использовать модуль , достаточно знать только его интерфейс: детали реализации модуля скрыты от его пользователя. Это позволяет успешно создавать программы большого объема, поскольку мозг человека может хранить одновременно довольно ограниченный объем информации. Кроме того, использование модулей позволяет преодолеть ограничение в один сегмент на объем кода исполняемой программы, поскольку код каждого подключаемого к программе модуля содержится в отдельном сегменте.
Модули можно разделить на стандартные, которые входят в состав системы программирования, и пользовательские, то есть создаваемые программистом. Чтобы подключить модуль к программе, его требуется предварительно скомпилировать. Результат компиляции каждого модуля хранится на диске в отдельном файле с расширением .tpu.
Описание модулей
Исходный текст каждого модуля хранится в отдельном файле с расширением .pas. Модуль состоит из секций (разделов). Общая структура модуля:
Модуль может использовать другие модули, для этого их надо перечислить в операторе uses , который может находиться только непосредственно после ключевых слов interface или implementation . Если модули подключаются к интерфейсной части, все константы и типы данных, описанные в интерфейсной секции этих модулей, могут использоваться в любом описании в интерфейсной части данного модуля. Если модули подключаются к части реализации, все описания из этих модулей могут использоваться только в секции реализации.
В интерфейсной секции модуля определяют константы, типы данных, переменные, а также заголовки процедур и функций. Полностью же подпрограммы описываются в секции реализации, скрытой от пользователя модуля. Это естественно, поскольку для применения подпрограммы требуется знать только информацию, которая содержится в ее заголовке.
В секции реализации описываются подпрограммы, заголовки которых приведены в интерфейсной части. Заголовок подпрограммы должен или быть идентичным указанному в секции интерфейса, или состоять только из ключевого слова procedure или function и имени подпрограммы. Для функции также указывается ее тип.
Кроме того, в этой секции можно определять константы, типы данных, переменные и внутренние подпрограммы. Они используются внешними элементами модуля и видны только в секции реализации.
Секция инициализации предназначена для присваивания начальных значений переменным, используемым в модуле или в программе, к которой он подключен. Операторы, расположенные в секции инициализации модуля, выполняются перед операторами основной программы. Если к программе подключено более одного модуля, их секции инициализации вызываются на выполнение в порядке, указанном в операторе uses .
В оболочках Borland Pascal и Turbo Pascal результат компиляции по умолчанию размещается в оперативной памяти и на диск не записывается. Поэтому для сохранения скомпилированного модуля на диске требуется установить значение пункта Compile ( Destination в значение Disk. Компилятор создаст файл с расширением .tpu, который надо переместить в специальный каталог, путь к которому указан в пункте меню Options ( Directories в поле Unit Directories.
В качестве примера оформим в виде модуля подпрограмму вычисления среднего арифметического значения элементов массива из пример 4.1 ( пример 4.6).
Список параметров подпрограммы в разделе реализации указывать не обязательно.
Что такое модульное программирование и кому оно нужно
В любой профессии, не только в программировании, вы переживаете разные эмоциональные состояния по ходу выполнения проекта:
- Сначала есть энтузиазм от перспектив и возможностей.
- Затем приходит азарт. Первые ошибки и трудности вас только раззадоривают, заставляя мозг и фантазию работать на полную катушку.
- Следом проседает концентрация. В какой-то момент вы перестаёте обращать внимание на предупреждения и мелкие ошибки, откладывая решение этих проблем на потом.
- В итоге вы теряете мотивацию. Вы исправляете одну ошибку – появляется три. Вы пытаетесь добавить новую функцию, но выкидываете идею в мусорное ведро из-за нежелания тратить на это много времени.
Некоторые думают, что это нормально: стоит смириться и каждый раз проживать этот цикл. На деле же всё немного проще, и решение лежит не в области психологии, а в подходе к созданию кода.
Классическая проблема программирования
В западной литературе существует термин «big ball of mud» для описания архитектуры программы. Давайте переведём его дословно. Графически «большой шар грязи» можно представить в виде точек на окружности, символизирующих функциональные элементы, и прямых – связей между ними:
Похоже на ваши глаза перед сдачей проекта, не так ли?
Это иллюстрация той сложности, с которой вам надо работать, какое количество связей учитывать, если возникает ошибка.
Программирование не уникальная дисциплина: здесь можно и нужно применять опыт из других областей. Возьмём, к примеру, компьютер. Их производители не задумываются над многообразием задач, которые решает пользователь, и уж тем более не выделяют под каждую маленький процессор и память. Компьютер – это просто набор независимых сложных объектов, объединённых в одном корпусе при помощи разъёмов и проводов. Объекты не уникальны, не оптимизированы конкретно под вас, и тем не менее блестяще справляются со своей задачей.
В программировании есть точно такие же решения. Например, библиотеки. Они помогают не тратить драгоценное время на изобретение велосипеда. Однако для частных задач библиотеки не эффективны – создание отнимет уйму времени, а при единичной повторяемости эффективность стремится к нулю.
В этом случае полезнее обратиться к модулям. Модуль – логически завершённый фрагмент кода, имеющий конкретное функциональное назначение. Для взаимодействия модулей используются способы, не позволяющие изменять параметры и функциональность. Плюсы модульного программирования очевидны:
- Ускорение разработки.
- Повышение надёжности.
- Упрощение тестирования.
- Взаимозаменяемость.
Модульное программирование крайне эффективно при групповых разработках, где каждый сотрудник может сконцентрироваться только на своём фронте работ и не оглядываться на решения коллег. Однако и в индивидуальном подходе вы получаете, как минимум, вышеописанные преимущества.
Но не всё так просто.
Проблемы модульного программирования
Сама по себе идея использования модулей не сильно упрощает код, важно минимизировать количество прямых связей между ними. Здесь мы подходим к понятию «инверсия управления» (IoC). Упрощённо – это принцип программирования, при котором отдельные компоненты кода максимально изолированы друг от друга. То есть детали одного модуля не должны влиять на реализацию другого. Достигается это при помощи интерфейсов или других видов представления, не обеспечивающих прямого доступа к модульному коду.
В повседневной жизни таких примеров множество. Чтобы купить билет на самолёт или узнать время вылета, вам не надо звонить пилоту. Чтобы выпить молока, не надо ехать в деревню или на завод и стоять над душой у коровы. Для этого всегда есть посредники.
В модульном программировании существует три основные реализации:
- Внедрение зависимостей. Способ, при котором каждый элемент имеет свой интерфейс, взаимодействие модулей происходит через интерфейсы.
- Фабричный метод. Основывается на существовании некого объекта, предназначенного для создания других объектов. Иначе говоря, введение в программу прототипа, объединяющего общие черты для большинства объектов. Прямого взаимодействия между модулями нет, все параметры наследуются от «завода».
- Сервисный метод. Создаётся один общий интерфейс, являющийся буфером для взаимодействия объектов. Похожую функцию в реальной жизни выполняют колл-центры, магазины, площадки для объявлений и т.д.
Несмотря на то, что первая реализация IoC используется чаще всего, для первых шагов в модульном программировании лучше использовать другие два. Причина – простое создание интерфейсов лишь ограничивает доступ к модулям, а для снижения сложности кода необходимо также уменьшить количество связей. Интерфейсы, хаотично ссылающиеся на другие интерфейсы, код только усложняют.
Для решения этой проблемы необходимо разработать архитектуру кода. Как правило, она схожа с файловой структурой любого приложения:
Таким образом, поддержка принципов модульного программирования, инверсии управления и четкой архитектуры приложения поможет убить сразу трёх зайцев:
- Обеспечить чёткое функциональное разделение кода. При возникновении ошибок можно быстро определить источник, а исправления не приведут к появлению новых сбоев.
- Минимизировать количество связей. Это позволит упростить разработку, отдав на откуп нескольким разработчикам разные модули. Или вы сможете самостоятельно разрабатывать каждый блок без оглядки на другие, что тоже экономит время и силы.
- Создать иерархию с чёткой вертикалью наследования. Это повышает надёжность кода, так как тестирование провести проще, а результаты информативнее.
Соблюдение принципа модульности в больших проектах позволит сэкономить время и не расплескать стартовый задор. Более того, у вас получится наконец сосредоточиться на самом интересном – реализации оригинальных задумок в коде. А ведь это именно то, что каждый из нас ищет в программировании.
В любой профессии, не только в программировании, вы переживаете разные эмоциональные состояния по ходу выполнения проекта:
- Сначала есть энтузиазм от перспектив и возможностей.
- Затем приходит азарт. Первые ошибки и трудности вас только раззадоривают, заставляя мозг и фантазию работать на полную катушку.
- Следом проседает концентрация. В какой-то момент вы перестаёте обращать внимание на предупреждения и мелкие ошибки, откладывая решение этих проблем на потом.
- В итоге вы теряете мотивацию. Вы исправляете одну ошибку – появляется три. Вы пытаетесь добавить новую функцию, но выкидываете идею в мусорное ведро из-за нежелания тратить на это много времени.
Некоторые думают, что это нормально: стоит смириться и каждый раз проживать этот цикл. На деле же всё немного проще, и решение лежит не в области психологии, а в подходе к созданию кода.
Классическая проблема программирования
В западной литературе существует термин «big ball of mud» для описания архитектуры программы. Давайте переведём его дословно. Графически «большой шар грязи» можно представить в виде точек на окружности, символизирующих функциональные элементы, и прямых – связей между ними:
Похоже на ваши глаза перед сдачей проекта, не так ли?
Это иллюстрация той сложности, с которой вам надо работать, какое количество связей учитывать, если возникает ошибка.
Программирование не уникальная дисциплина: здесь можно и нужно применять опыт из других областей. Возьмём, к примеру, компьютер. Их производители не задумываются над многообразием задач, которые решает пользователь, и уж тем более не выделяют под каждую маленький процессор и память. Компьютер – это просто набор независимых сложных объектов, объединённых в одном корпусе при помощи разъёмов и проводов. Объекты не уникальны, не оптимизированы конкретно под вас, и тем не менее блестяще справляются со своей задачей.
В программировании есть точно такие же решения. Например, библиотеки. Они помогают не тратить драгоценное время на изобретение велосипеда. Однако для частных задач библиотеки не эффективны – создание отнимет уйму времени, а при единичной повторяемости эффективность стремится к нулю.
В этом случае полезнее обратиться к модулям. Модуль – логически завершённый фрагмент кода, имеющий конкретное функциональное назначение. Для взаимодействия модулей используются способы, не позволяющие изменять параметры и функциональность. Плюсы модульного программирования очевидны:
- Ускорение разработки.
- Повышение надёжности.
- Упрощение тестирования.
- Взаимозаменяемость.
Модульное программирование крайне эффективно при групповых разработках, где каждый сотрудник может сконцентрироваться только на своём фронте работ и не оглядываться на решения коллег. Однако и в индивидуальном подходе вы получаете, как минимум, вышеописанные преимущества.
Но не всё так просто.
Проблемы модульного программирования
Сама по себе идея использования модулей не сильно упрощает код, важно минимизировать количество прямых связей между ними. Здесь мы подходим к понятию «инверсия управления» (IoC). Упрощённо – это принцип программирования, при котором отдельные компоненты кода максимально изолированы друг от друга. То есть детали одного модуля не должны влиять на реализацию другого. Достигается это при помощи интерфейсов или других видов представления, не обеспечивающих прямого доступа к модульному коду.
В повседневной жизни таких примеров множество. Чтобы купить билет на самолёт или узнать время вылета, вам не надо звонить пилоту. Чтобы выпить молока, не надо ехать в деревню или на завод и стоять над душой у коровы. Для этого всегда есть посредники.
В модульном программировании существует три основные реализации:
- Внедрение зависимостей. Способ, при котором каждый элемент имеет свой интерфейс, взаимодействие модулей происходит через интерфейсы.
- Фабричный метод. Основывается на существовании некого объекта, предназначенного для создания других объектов. Иначе говоря, введение в программу прототипа, объединяющего общие черты для большинства объектов. Прямого взаимодействия между модулями нет, все параметры наследуются от «завода».
- Сервисный метод. Создаётся один общий интерфейс, являющийся буфером для взаимодействия объектов. Похожую функцию в реальной жизни выполняют колл-центры, магазины, площадки для объявлений и т.д.
Несмотря на то, что первая реализация IoC используется чаще всего, для первых шагов в модульном программировании лучше использовать другие два. Причина – простое создание интерфейсов лишь ограничивает доступ к модулям, а для снижения сложности кода необходимо также уменьшить количество связей. Интерфейсы, хаотично ссылающиеся на другие интерфейсы, код только усложняют.
Для решения этой проблемы необходимо разработать архитектуру кода. Как правило, она схожа с файловой структурой любого приложения:
Таким образом, поддержка принципов модульного программирования, инверсии управления и четкой архитектуры приложения поможет убить сразу трёх зайцев:
- Обеспечить чёткое функциональное разделение кода. При возникновении ошибок можно быстро определить источник, а исправления не приведут к появлению новых сбоев.
- Минимизировать количество связей. Это позволит упростить разработку, отдав на откуп нескольким разработчикам разные модули. Или вы сможете самостоятельно разрабатывать каждый блок без оглядки на другие, что тоже экономит время и силы.
- Создать иерархию с чёткой вертикалью наследования. Это повышает надёжность кода, так как тестирование провести проще, а результаты информативнее.
Соблюдение принципа модульности в больших проектах позволит сэкономить время и не расплескать стартовый задор. Более того, у вас получится наконец сосредоточиться на самом интересном – реализации оригинальных задумок в коде. А ведь это именно то, что каждый из нас ищет в программировании.
Теоретический материал. Модульное программирование;
Модульное программирование
Лекция №10
Вопросы для контроля
- Перечислите известные Вам методы проектирования программ
- Какие методы программирования Вам известны? В чем их суть?
- В чем заключается метод декларативного и логического проектирования?
- Охарактеризуйте метод логического и метод функционального программирования
- Чем отличаются метод структурного проектирования и метод создания проекта?
Обучающая: получить представление о модульном программировании, о понятии «модуль», его характеристиках, видах модулей, о методах разработки, применяемых при модульном программировании;
Ведущий метод обучения: объяснительно-иллюстративный.
Оснащение занятия: конспект лекции, презентации.
Для обеспечения технологичности разрабатываемого программного обеспечения применяется модульное программирование.
1. Понятие модуля
Приступая к разработке программы, следует иметь ввиду, что она, как правило, является большой системой, поэтому необходимо принять меры для ее упрощения. Для этого программу разрабатывают по частям, которые называются программными модулями. Такой метод создания программ называется модульным программированием.
Модульное программирование основано на понятии модуля – программы или функционально завершенного фрагмента программы.
• один вход и один выход. На входе программный модуль получает определенный набор исходных данных, выполняет их обработку и возвращает один набор выходных данных;
• функциональная завершенность. Модуль выполняет набор определенных операций для реализации каждой отдельной функции, достаточных для завершения начатой обработки данных;
• логическая независимость. Результат работы данного фрагмента программы не зависит от работы других модулей;
• слабые информационные связи с другими программными модулями. Обмен информацией между отдельными модулями должен быть минимален;
Таким образом, модули содержат описание исходных данных, операции обработки данных и структуры взаимосвязи с другими модулями.
Программный модуль является самостоятельным программным продуктом. Это означает, что каждый программный модуль разрабатывается, компилируется и отлаживается отдельно от других модулей программы. Более того, каждый разработанный программный модуль может включаться в состав разных программных систем при условии выполнения требований, предъявляемых к его использованию в документации к этому модулю. Таким образом, программный модуль может рассматриваться и как средство упрощения сложных программ, и как средство накопления и многократного использования программистских знаний.
2. Основные характеристики программного модуля
В литературе приводятся различные критерии оценки приемлемости модуля. Были предложены следующие критерии:
• хороший модуль снаружи проще, чем внутри;
• хороший модуль проще использовать, чем построить.
Предлагается использовать следующие характеристики программного модуля для оценки его приемлемости: размер модуля, прочность модуля, сцепление с другими модулями и рутинность модуля.
Размер модуля измеряется числом содержащихся в нем операторов. Модуль не должен быть слишком маленьким или слишком большим. Большие модули, как правило, сложны для понимания и неудобны для внесения изменений, они могут существенно увеличить суммарное время повторных трансляций программы при отладке. Маленькие модули усложняют общую структурную схему программы и могут не окупать накладных расходов, связанных с их оформлением. Обычно рекомендуются программные модули размером от нескольких десятков до нескольких сотен операторов.
Прочность модуля — это мера его внутренних связей. Чем выше прочность модуля, тем больше связей скрыто от внешней по отношению к нему части программы и, следовательно, тем проще сама программа. Самой слабой степенью прочности обладает модуль, прочный по совпадению. В данном случае в программный модуль оформляется повторяющаяся в нескольких местах программы последовательность операторов. Если вдруг возникнет необходимость изменения этой последовательности в одном из контекстов, придется изменять сам модуль, что может сделать его использование в других контекстах ошибочным. Такой класс программных модулей не рекомендуется для использования.
Функционально прочный модуль — это модуль, реализующий одну какую-либо определенную функцию. При этом он может использовать и другие модули. Такой вид прочности модулей рекомендуется для использования.
Высшей степенью прочности обладает информационно прочный модуль — это модуль, выполняющий несколько операций над одной и той же структурой данных, которая неизвестна вне этого модуля. Для каждой из этих операций в таком модуле имеется свой вход со своей формой обращения к нему. Информационно прочный модуль может реализовывать, например абстрактный тип данных.
Сцепление модуля — это мера его зависимости по способу передачи данных от других модулей. Чем слабее сцепление модуля с другими модулями, тем сильнее его независимость от других модулей. Для оценки степени сцепления существует шесть видов сцепления модулей по:
• общей области данных;
Худшим видом сцепления модулей является сцепление по содержимому. Таким является сцепление двух модулей, когда один из них имеет прямые ссылки на содержимое другого модуля (например, на константу, содержащуюся в другом модуле). Такое сцепление модулей недопустимо.
Не рекомендуется использовать также сцепление по общей области — это такое сцепление модулей, когда несколько модулей используют одну и ту же область памяти.
Сцепление по образцу предполагает, что модули обмениваются данными, объединенными в структуры. Этот тип обеспечивает неплохие характеристики по сравнению с предыдущими. Недостаток заключается в том, что конкретные передаваемые данные «спрятаны» в структуры, и потому уменьшается «прозрачность» связи между модулями. Кроме того, при изменении структуры передаваемых данных необходимо модифицировать все использующие ее модули.
При сцеплении по управлению один модуль посылает другому некоторый информационный объект (флаг), предназначенный для управления внутренней логикой модуля. Таким способом часто выполняют настройку режимов работы программного обеспечения. Подобные настройки также снижают наглядность взаимодействия модулей и потому обеспечивают не лучшие характеристики технологичности разрабатываемого программного обеспечения.
Сцепление по внешним ссылкам предполагает, что модули ссылаются на один и тот же глобальный элемент данных.
Единственным видом сцепления модулей, который рекомендуется для использования современной технологией программирования, является сцепление по данным (параметрическое сцепление) — это случай, когда данные передаются модулю либо при обращении к нему как значения его параметров, либо как результат его обращения к другому модулю для вычисления некоторой функции. Такой вид сцепления модулей реализуется на языках программирования при использовании обращений к процедурам (функциям).
Рутинность модуля — это его независимость от предыстории обращений к нему. Модуль будем называть рутинным, если результат обращения к нему зависит только от значений его параметров и не зависит от результатов предыдущих обращений к нему.Модуль будем называть зависящим от предыстории, если результат обращения к нему зависит от внутреннего состояния этого модуля, хранящего следы предыдущих обращений к нему. В книге Майерса не рекомендуется использовать зависящие от предыстории модули, так как они провоцируют появление в программах неуловимых ошибок. Однако во многих случаях именно зависящий от предыстории модуль является наиболее информационно прочным. Поэтому более приемлема следующая рекомендация:
• всегда следует использовать рутинный модуль, если это не приводит к плохим сцеплениям модулей;
• зависящие от предыстории модули следует использовать только в случае, когда это необходимо для обеспечения параметрического сцепления;
• в спецификации зависящего от предыстории модуля должна быть четко сформулирована эта зависимость таким образом, чтобы было возможно прогнозировать поведение данного модуля при разных последующих обращениях к нему.
Связность модулей — мера прочности соединения функциональных и информационных объектов внутри одного модуля. Размещение сильно связанных элементов в одном модуле уменьшает межмодульные связи, в то время как помещение сильно связанных элементов в разные модули не только усиливает межмодульные связи, но и усложняет понимание их взаимодействия. Объединение слабо связанных элементов также уменьшает технологичность модулей, делая их сложнее для понимания.
Различают следующие виды связности (в порядке убывания уровня):
При функциональной связности модуль предназначен для выполнения одной функции. Его исходные данные и операции предназначены для решения одной конкретной задачи. Такой модуль имеет максимальную связность и, как следствие, хорошую технологичность (простота компиляции, тестирования, сопровождения).
При последовательной связности модуля результат обработки данных одной функцией служит исходными данными для другой функции. Такой модуль реализует одну подпрограмму, выполняющую две функции. Модуль с последовательной связностью функций можно разбить на два модуля или более, как с последовательной, так и с функциональной связностью. При этом данные, используемые последовательными функциями, также связаны последовательно. Такой модуль выполняет несколько функций, и, следовательно, его технологичность хуже с точки зрения понимания и тестирования.
Информационно связанными считают функции, обрабатывающие одни и те же данные. Информационно связанный модуль имеет неплохие показатели технологичности, так как все функции, работающие с одними и теми же данными, собраны в один модуль, что позволяет при изменении формата данных корректировать только его. Данные, которые обрабатываются одной функцией, также считают информационно связанными.
Процедурно связаны функции или данные, которые являются частями одного процесса. При процедурной связности отдельные элементы модуля связаны крайне слабо, так как реализуемые ими операции связаны лишь общим процессом, следовательно, технологичность такого модуля хуже, чем у предыдущих.
Временная связность функций подразумевает, что эти функции выполняются параллельно или в течение некоторого периода времени. Временная связность данных означает, что они используются в некотором временном интервале. Отличительной особенностью временной связности является то, что действия, реализуемые такими функциями, обычно могут выполняться в любом порядке. Например, временную связность имеют функции, выполняемые при инициализации некоторого процесса. Большая вероятность модификации функции еще больше уменьшает показатели технологичности модулей данного вида по сравнению с предыдущими, кроме того, содержание модуля с временной связностью функций может изменяться: в него могут включаться новые действия и/или исключаться старые.
Логическая связь строится на основе объединения данных или функций в одну логическую группу, например, логически связаны компоненты модуля, содержащего функции обработки текстовой информации или данные одного и того же типа. При выполнении модуля с логически связанными компонентами всегда будет вызываться одна какая-либо его часть, при этом вызывающий и вызываемый модули будут связаны по управлению. Показатели технологичности таких модулей ниже предыдущих, так как сложно понять логику их работы.
Модуль, элементы которого имеют случайную связность, имеет самые низкие показатели технологичности, так как его элементы вообще не связаны.
В табл. 2 представлены характеристики различных видов связности по экспертным оценкам
Таблица 2. Сравнительные характеристики различных видов связности