Для многих небольших программ достаточно написать один модуль содержащий весь код. Затем его можно компилировать, линковать и исполнять.
Однако для более или менее крупных приложений становится невозможным поддерживать один модуль со всем необходимым. Либо, если даже это технически возможно, компиляция такого модуля при каждом изменении будет отнимать очень много времени. И с этого момента становится необходимым разбить программу на несколько частей (модулей).
Разделить программу очень просто. Если единственной целью является уменьшение размера модулей, необходимых для компиляции, то простого разделения кода по модулям будет достаточно.
Однако существуют другие преимущества в модульности программы. Вот некоторые из них:
В следующих разделах каждый из этих пунктов обсуждается более подробно.
Как сказано выше, простое разбиение программы на несколько частей, уменьшит время необходимое для перекомпиляции исходного кода. Исправление ошибки кодирования часто требует изменения только одной или двух строк. Перекомпиляция небольшой части кода и перелинковка произойдёт быстрее, чем перекомпиляция всего вместе.
Иногда может потребоваться перекомпиляция всех модулей. Обычно это необходимо когда изменяются используемые несколькими модулями структуры данных, константы, макро и др. В хорошем проекте программы такие изменения происходят в заголовочном файле, и перекомпилироваться будут все модули, которые включают этот файл.
Лучший способ разбить программу на модули — предназначить каждый модуль для каких-то определённых целей. Например, один модуль может заниматься только взаимодействием с пользователем. Другой модуль может обслуживать таблицу имён, а ещё один может обрабатывать небольшое подмножество из всех действий программы.
Многие модули становятся менеджерами ресурса, и каждая часть кода, которой требуется сделать что-то с этим ресурсом, должна работать посредством этого менеджера.
Используя пример с менеджером таблицы имён, менеджеру, вероятно, придётся делать что-то вроде создания и удаления входа таблицы для имени. Эти действия станут двумя функциями с внешней связью.
Если разделить программу на части со связанной функциональностью, то обычно проще решить, где искать источник возникшей проблемы.
Имена модулей, которые ясно формулируют их предназначение также помогают искать нужную информацию.
Иногда модуль написан так, что он единолично владеет некоторыми структурами данных, такими как связанный список. Всем другим модулям для доступа к таким данным нужно вызывать функцию из модуля, который ими владеет. Эта техника известна как скрытие данных. Реальные данные скрыты в структуре и для доступа к ним можно использовать только функциональный интерфейс (также называемый процедурным). Функциональный интерфейс — просто набор функций, предоставленный для доступа к структуре.
Основное преимущество скрытия данных заключается в том, что структура может быть изменена с небольшим или вообще без влияния на другие модули. К тому же, так как доступ к структуре контролируется, то уменьшается количество ошибок в результате неправильного её использования.
Есть возможность иметь разные уровни скрытия данных. Полное получается, когда ни один внешний модуль вообще не имеет доступа к структуре. Частичное скрытие получается, когда имеется доступ к элементам структуры, но не ко всей структуре.
Помните, что эти правила работают только, если программист их уважает. Они не навязаны компилятором. Если модуль включает заголовочный файл, в котором описана структура данных, используемая другим модулем, который хочет иметь исключительный доступ к этой структуре, то правило нарушается. Плохо это или хорошо — решать программисту.
С полным скрытием данных указатель на структуру данных используется только как параметр функционального интерфейса. Для получения или установки значения в структуре потребуется вызов функции.
Преимущество данной техники в том, что структура данных может быть полностью
переработана без влияния на другие модули. Определения внутренних структур
(таких как struct
, union
или массивов) могут быть
изменены, при этом другие модули изменять или даже перекомпилировать не
придётся.
Основной недостаток полного скрытия данных в том, что даже простые обращения требуют вызова функции, что менее эффективно, чем ссылка на место в памяти.
Для реализации полного скрытия данных можно использовать макро, подобные функции, избегая этим вызовов функций, но скрывая истинную структуру данных. При изменении структуры данных потребуется перекомпиляция всех модулей.
Частичное скрытие данных получается, когда вся структура целиком (например, связанный список) недоступна, но доступны её элементы (элемент связанного списка).
Если использовать пример с менеджером таблицы имён, то, возможно, его нужно
будет вызвать для создания входа для имени, а после этого будет возвращён
указатель на имя как результат функции create
. Он будет
указывать на структуру определённую в заголовочном файле, который может быть
включён любым модулем. Поэтому можно напрямую манипулировать данными в
элементе этой структуры.
Этот метод более эффективен, чем полное скрытие данных. Однако, когда изменится структура, использованная для таблицы имён, все модули, которые на неё ссылаются придётся перекомпилировать.
С использованием модульного дизайна программы и скрытия данных часто возможно
полностью заменить модуль без влияния на другие. Обычно это возможно только,
когда не изменяется функциональный интерфейс. С частичным скрытием данных
актуальные типы использованные для реализации структуры должны оставаться без
изменений, иначе понадобится, по меньшей мере, перекомпиляция. Изменение типа
в struct
, например, может потребовать перекомпиляции, если
изменены только типы членов или добавлены новые. Однако, если изменены имена
членов, или произведены другие важные изменения, то в других модулях придётся
менять исходный код.
Зависимость от системы играет роль только, когда программа разрабатывается для работы на разных компьютерах или под управлением разных операционных систем. Изолирование системно-зависимого кода более подробно рассматривается в разделе Написание переносимых программ.
Безусловно, иногда трудно решить, что является системно-зависимым кодом. В первый раз, когда программа переносится на новую систему, обычно возникают некоторые проблемы. Необходимо тщательно проверить эти места, и код, который зависит от окружения, изолировать. Это можно сделать поместив код в отдельный модуль, помеченный как системно-зависимый, либо разместить в коде макро, чтобы компиляция происходила по-разному для разных систем.