В чем состоит особенность модульного программирования
Что такое модульное программирование и кому оно нужно
В любой профессии, не только в программировании, вы переживаете разные эмоциональные состояния по ходу выполнения проекта:
Некоторые думают, что это нормально: стоит смириться и каждый раз проживать этот цикл. На деле же всё немного проще, и решение лежит не в области психологии, а в подходе к созданию кода.
Классическая проблема программирования
В западной литературе существует термин «big ball of mud» для описания архитектуры программы. Давайте переведём его дословно. Графически «большой шар грязи» можно представить в виде точек на окружности, символизирующих функциональные элементы, и прямых – связей между ними:
Похоже на ваши глаза перед сдачей проекта, не так ли?
Это иллюстрация той сложности, с которой вам надо работать, какое количество связей учитывать, если возникает ошибка.
Программирование не уникальная дисциплина: здесь можно и нужно применять опыт из других областей. Возьмём, к примеру, компьютер. Их производители не задумываются над многообразием задач, которые решает пользователь, и уж тем более не выделяют под каждую маленький процессор и память. Компьютер – это просто набор независимых сложных объектов, объединённых в одном корпусе при помощи разъёмов и проводов. Объекты не уникальны, не оптимизированы конкретно под вас, и тем не менее блестяще справляются со своей задачей.
В программировании есть точно такие же решения. Например, библиотеки. Они помогают не тратить драгоценное время на изобретение велосипеда. Однако для частных задач библиотеки не эффективны – создание отнимет уйму времени, а при единичной повторяемости эффективность стремится к нулю.
В этом случае полезнее обратиться к модулям. Модуль – логически завершённый фрагмент кода, имеющий конкретное функциональное назначение. Для взаимодействия модулей используются способы, не позволяющие изменять параметры и функциональность. Плюсы модульного программирования очевидны:
Модульное программирование крайне эффективно при групповых разработках, где каждый сотрудник может сконцентрироваться только на своём фронте работ и не оглядываться на решения коллег. Однако и в индивидуальном подходе вы получаете, как минимум, вышеописанные преимущества.
Но не всё так просто.
Проблемы модульного программирования
Сама по себе идея использования модулей не сильно упрощает код, важно минимизировать количество прямых связей между ними. Здесь мы подходим к понятию «инверсия управления» (IoC). Упрощённо – это принцип программирования, при котором отдельные компоненты кода максимально изолированы друг от друга. То есть детали одного модуля не должны влиять на реализацию другого. Достигается это при помощи интерфейсов или других видов представления, не обеспечивающих прямого доступа к модульному коду.
В повседневной жизни таких примеров множество. Чтобы купить билет на самолёт или узнать время вылета, вам не надо звонить пилоту. Чтобы выпить молока, не надо ехать в деревню или на завод и стоять над душой у коровы. Для этого всегда есть посредники.
В модульном программировании существует три основные реализации:
Несмотря на то, что первая реализация IoC используется чаще всего, для первых шагов в модульном программировании лучше использовать другие два. Причина – простое создание интерфейсов лишь ограничивает доступ к модулям, а для снижения сложности кода необходимо также уменьшить количество связей. Интерфейсы, хаотично ссылающиеся на другие интерфейсы, код только усложняют.
Для решения этой проблемы необходимо разработать архитектуру кода. Как правило, она схожа с файловой структурой любого приложения:
Соблюдение принципа модульности в больших проектах позволит сэкономить время и не расплескать стартовый задор. Более того, у вас получится наконец сосредоточиться на самом интересном – реализации оригинальных задумок в коде. А ведь это именно то, что каждый из нас ищет в программировании.
В любой профессии, не только в программировании, вы переживаете разные эмоциональные состояния по ходу выполнения проекта:
Некоторые думают, что это нормально: стоит смириться и каждый раз проживать этот цикл. На деле же всё немного проще, и решение лежит не в области психологии, а в подходе к созданию кода.
Классическая проблема программирования
В западной литературе существует термин «big ball of mud» для описания архитектуры программы. Давайте переведём его дословно. Графически «большой шар грязи» можно представить в виде точек на окружности, символизирующих функциональные элементы, и прямых – связей между ними:
Похоже на ваши глаза перед сдачей проекта, не так ли?
Это иллюстрация той сложности, с которой вам надо работать, какое количество связей учитывать, если возникает ошибка.
Программирование не уникальная дисциплина: здесь можно и нужно применять опыт из других областей. Возьмём, к примеру, компьютер. Их производители не задумываются над многообразием задач, которые решает пользователь, и уж тем более не выделяют под каждую маленький процессор и память. Компьютер – это просто набор независимых сложных объектов, объединённых в одном корпусе при помощи разъёмов и проводов. Объекты не уникальны, не оптимизированы конкретно под вас, и тем не менее блестяще справляются со своей задачей.
В программировании есть точно такие же решения. Например, библиотеки. Они помогают не тратить драгоценное время на изобретение велосипеда. Однако для частных задач библиотеки не эффективны – создание отнимет уйму времени, а при единичной повторяемости эффективность стремится к нулю.
В этом случае полезнее обратиться к модулям. Модуль – логически завершённый фрагмент кода, имеющий конкретное функциональное назначение. Для взаимодействия модулей используются способы, не позволяющие изменять параметры и функциональность. Плюсы модульного программирования очевидны:
Модульное программирование крайне эффективно при групповых разработках, где каждый сотрудник может сконцентрироваться только на своём фронте работ и не оглядываться на решения коллег. Однако и в индивидуальном подходе вы получаете, как минимум, вышеописанные преимущества.
Но не всё так просто.
Проблемы модульного программирования
Сама по себе идея использования модулей не сильно упрощает код, важно минимизировать количество прямых связей между ними. Здесь мы подходим к понятию «инверсия управления» (IoC). Упрощённо – это принцип программирования, при котором отдельные компоненты кода максимально изолированы друг от друга. То есть детали одного модуля не должны влиять на реализацию другого. Достигается это при помощи интерфейсов или других видов представления, не обеспечивающих прямого доступа к модульному коду.
В повседневной жизни таких примеров множество. Чтобы купить билет на самолёт или узнать время вылета, вам не надо звонить пилоту. Чтобы выпить молока, не надо ехать в деревню или на завод и стоять над душой у коровы. Для этого всегда есть посредники.
В модульном программировании существует три основные реализации:
Несмотря на то, что первая реализация IoC используется чаще всего, для первых шагов в модульном программировании лучше использовать другие два. Причина – простое создание интерфейсов лишь ограничивает доступ к модулям, а для снижения сложности кода необходимо также уменьшить количество связей. Интерфейсы, хаотично ссылающиеся на другие интерфейсы, код только усложняют.
Для решения этой проблемы необходимо разработать архитектуру кода. Как правило, она схожа с файловой структурой любого приложения:
Соблюдение принципа модульности в больших проектах позволит сэкономить время и не расплескать стартовый задор. Более того, у вас получится наконец сосредоточиться на самом интересном – реализации оригинальных задумок в коде. А ведь это именно то, что каждый из нас ищет в программировании.
В чем состоит особенность модульного программирования
Самый верхний уровень организации программы касается только достаточно больших проектов. Это разделение программы на более-менее независимые части (модули), их независимое проектирование и трансляция.
Иерархия
Любая сложная система не обходится без иерархии, без нее большая система превращается в нечто аморфное, необозримое и слабо управляемое.
· вся программа в целом образуют проект. В интегрированных системах проект и все его модули могут быть представлены одним файлом. В традиционных системах программирования (к ним относится и Си/Си++) проект состоит из файлов исходного текста – модулей (обычные текстовые файлы), файла проекта, содержащего список модулей, настройки транслятора и т.п., а также вспомогательных файлов. В этом случае под проект отводится отдельная папка.
Способы модульной организации программы и взаимодействия ее частей в значительной степени обусловлены особенностями трансляции программы, поэтому здесь необходимы минимальные знания о трансляции и связывании программы и ее элементов.
Сущность трансляции. Компиляция и интерпретация
Под трансляцией в самом широком смысле можно понимать процесс восприятия компьютером программы, написанной на некотором формальном языке. При всем своем различии языки программирования имеют много общего и, в принципе, эквиваленты с точки зрения потенциальной возможности написать одну и ту же программу на любом из них. На самом деле сложно подвести под одну схему имеющееся многообразие языков программирования,
Следовательно, компиляция и интерпретация отличаются не характером и методами анализа и преобразования объектов программы, а совмещением фаз обработки этих объектов во времени. То есть при компиляции фазы преобразования и выполнения действий разнесены во времени, но зато каждая из них выполняется над всеми объектами программы одновременно. При интерпретации, наоборот, преобразование и выполнение действий объединены во времени, но для каждого объекта программы.
· для выполнения программы, написанной на определенном формальном языке после ее компиляции необходим интерпретатор, выполняющий эту программу, но уже записанную на выходном языке компилятора;
· процессор и память любого компьютера (а в широком смысле и вся программная среда, создаваемая операционной системой, является интерпретатором машинного кода);
· в практике построения трансляторов часто встречается случай, когда программа компилируется с входного языка на некоторый промежуточный уровень (внутренний язык), для которого имеется программный интерпретатор. Многие языковые системы программирования, называемые интерпретаторами, на самом деле имеют фазу компиляции во внутренне представление, на котором производится интерпретация.
Таким образом, граница между компиляцией и интерпретацией в трансляторе может перемещаться от входного языка (тогда мы имеем чистый интерпретатор) до машинного кода (тогда речь идет о чистом компиляторе).
Создание слоя программной интерпретации для некоторого промежуточного языка в практике построения трансляторов обычно встречается при попытке обеспечить совместимость для имеющегося многообразия языков программирования, операционных систем, архитектур и т.д. То есть определяется некоторый внутренний промежуточный язык, достаточно простой, чтобы для него можно было написать интерпретатор для всего имеющегося многообразия операционных систем или архитектур. Затем пишется одни (или несколько) компиляторов для одного (или нескольких) входных языков на этот промежуточный уровень. Приведем примеры такой стандартизации:
· для обеспечения совместимости и переносимости трансляторов на компьютеры с различной архитектурой или с различными операционными системами был разработан универсальный внутренний язык (P-код). Для каждой такой архитектуры необходимо реализовать свой интерпретатор P-кода. При этом все разнообразие имеющихся компиляторов с языков высокого уровня на P-код может быть использовано без каких-либо изменений.
Одним из существенных свойств «классического» Си является чистый программный код. Что это значит? Во-первых, транслятор представляет собой компилятор, генерирующий программный код целевого процессора. Во-вторых, транслятор «сознательно» не включает в этот код никаких дополнительных команд и обращений к внешним функциям, кроме явно прописанных в программе. То же самое касается и обрабатываемых данных: они имеют прямое представление в памяти без всяких дополнений или изменений. Все это гарантирует программе следующие свойства:
· программист контролирует эффективность полученного программного кода;
· программист контролирует размерности и размещение данных в памяти;
· программный код может выполняться без поддержки какой-либо операционной среды (исполнительной системы языка, библиотек, операционной системы), т.е. на «голой» ( standalone) машине.
Именно поэтому на классическом Си могут быть написаны такие компоненты, как программы для встроенных процессоров, ядро и драйверы операционных систем, т.е. то, что традиционно пишется на машинном языке (языке Ассемблера). Поэтому «классический» Си еще называют машинно-независимым Ассемблером.
Что же касается Си++, то там указанные принципы частично нарушаются, хотя он тоже является «чистым» компилятором, но не обеспечивает чистоту программного кода и данных.
Фазы трансляции и выполнения программы
Технология подготовки программ для языков компилирующего типа (к каковым относится Си/Си++) сформировалась в начале 60-х годов и с тех пор не претерпела существенных изменений. Заложенные тогда принципы оказывают влияние на способы использования стандартных библиотечных функций и разработки больших проектов.
При модульном проектировании весьма важна разница между определением и объявлением объектов программы (переменных, функций, методов, классов). Определение переменной или функции – это фрагмент программы, в котором полностью задано содержание объекта, и по которому происходит его трансляция во внутреннее представление. Объявление только упоминает объект языка и перечисляет его свойства, если он недоступен в данной точке программы. С учетом раздельного размещения определений и объявлений в проекте модульной Си-программы присутствуют три вида файлов (модулей):
· объектные модули (с расширением – obj ), полученные в результате независимой трансляции файлов исходного текста.
Препроцессор
Собственно говоря, препроцессор не имеет никакого отношения к языку. Это предварительная фаза трансляции, которая выполняет обработку текста программы, не вдаваясь глубоко в ее содержание. Он производит замену одних частей текста на другие, при этом сама программа так и остается в исходном виде. В языке Си директивы препроцессора оформлены отдельными строками программы, которые начинаются с символа «#». Здесь мы рассмотрим наиболее простые и популярные.
#define идентификатор строка_текста
Директива обеспечивает замену встречающегося в тексте программы идентификатора на соответствующую строку текста. Наиболее часто она применяется для символического обозначения константы, которая встречается многократно в различных частях программы. Например, размерность массива:
В данном примере вместо имени SIZE в текст программы будет подставлена строка, содержащая константу 100. Теперь, если нас не устраивает размерность массива, нам достаточно увеличить это значение в директиве define и повторно оттранслировать программу.
#define идентификатор(параметры) строка_с_параметрами
#define FOR(i,n) for(i=0; i
FOR(k,20) A[k]=0; // for(k=0; k
#include имя _ файла >
#include » имя _ файла «
включает в программу текст заголовочного файла, содержащего объявления внешних функций из библиотеки стандартного ввода-вывода.
Еще один полезное средство препроцессора – условная трансляция. Препроцессор способен устанавливать и проверять наличие определения ( define ) и значения как собственных переменных, так и переменных, содержащих параметры текущего окружения и характеристики транслятора. Например, можно исключить повторное включение кода директивой include, если включаемый текст обрамить такой конструкцией:
# ifndef AA // Код включается только при неопределенной переменной AA
# define AA 0 // Определить переменную препроцессора
Аналогичные средства в других языках программирования носят название макропроцессор, макросредства.
Трансляция и ее фазы
Самое главное в процессе трансляции состоит в том, что он не является линейным, то есть последовательным преобразованием фрагмента программы одного языка на другой. На процесс трансляции одного фрагмента обязательно оказывают влияние другие фрагменты программы. Потому в самом общем виде трансляция заключается в анализе текста программы и построения ее внутреннего представления (внутренней модели), из которой происходит синтез текста эквивалентной программы, но уже на другом языке.
Что касается анализа, то он происходит в три этапа, которые соответствуют трем основным составляющим любого языка программирования.
Фаза синтеза зависит от способа трансляции. В компиляторах он состоит в генерации кода, в интерпретаторах – в непосредственном исполнении (интерпретации) полученного внутреннего представления.
Модульное программирование, компоновка
· программный код, использующий в своей работе только объекты языка (типы данных, переменные, функции), определенные в текущем модуле, полностью переводится во внутреннее (двоичное) представление;
· если объект языка допускает внешний доступ из других модулей, то в объектом модуле создается точка входа, содержащая его имя и внутренний адрес в пространстве объектного модуля;
· при трансляции обращения к внешнему объекту языка объявление, полученное из заголовочного файла позволяет сформировать программный код для обращения к нему. Но все равно неизвестным остается его адрес. Поэтому вместо адреса транслятор оставляет внешнюю ссылку, содержащую исходное (символическое) имя объекта.
· объединение адресных пространств отдельных модулей (и их содержимого – внутреннего представления программы) в единое адресное пространство программного файла (компоновка);
· «соединение» внешних ссылок и соответствующих им точек входа (редактирование связей);
· при отсутствии необходимых точек входа для внешних ссылок их поиск производится в указанных библиотечных файлах. Если точка входа найдена в библиотеке объектных модулей, то весь объектный модуль, содержащий эту точку, компонуется в программу и для него повторяется описанный выше процесс.
В заключение отметим, что источником объектного модуля может быть не только Си-программа, но и программа, написанная на любом другом языке программирования, например, на Ассемблере. Но в этом случае необходимы дополнительные соглашения по поводу «стыковки» вызовов функций и обращений к данным в различных языках.
Понятие связывания. Статическое и динамическое связывание
· при определении языка;
· при реализации компилятора;
· во время трансляции;
· при компоновке (связывании);
· во время загрузки программы;
· во время выполнения программы, в том числе:
· при входе в модуль (процедуру, функцию);
· в произвольной точке выполнения программы.
В качестве примера рассмотрим простейший фрагмент программы, для которого перечислим более-менее полный перечень времен связывания его различных свойств с элементами архитектуры компьютера:
2. Конкретная размерность переменной int определяется при реализации соответствующего компилятора.
5. Если переменная определяется как внешняя (глобальная, вне тела функции), то смысл ее трансляции заключается в распределении под нее памяти в сегменте данных программы, который создается для текущего модуля (файла). Но при этом сама распределенной памяти к конкретной оперативной памяти осуществляется в несколько этапов:
· при трансляции переменная привязывается к некоторому относительному адресу в сегменте данных объектного модуля (то есть ее размещение фиксируется только относительно начала модуля)
· если программа работает не в физической, а в виртуальной памяти, то процесс загрузки может быть несколько иным. Программный модуль условно считается загруженным в некоторое виртуальное адресное пространство (с перемещением или без него как всей программы, так и отдельных ее сегментов). Реальная загрузка программы в память осуществляется уже в процессе работы программы по частям (сегментам, страницам), причем установление соответствия (или связывание) виртуальных и физических адресов осуществляется динамически операционной системой с использованием соответствующих аппаратных средств.
6. Если переменная определяется как автоматическая (локальная внутри тела функции или блока), то она размещается в стеке программы:
· во время трансляции определяется ее размерность и генерируются команды, которые резервируют под нее память в стеке в момент входа в тело функции (блок). То есть в процессе трансляции переменная связывается только с относительным адресом в стеке программы;
· связывание локальным переменной с ее адресом в сегменте стека осуществляется при выполнении в момент входа в тело функции (блок). Благодаря такому способу связывания в рекурсивной функции существует столько «экземпляров» локальных переменных, сколько раз функция вызывает сама себя.
В заключение отметим основные свойства Си с точки зрения понятий «связывание, статический, динамический»:
· язык Си является компилируемым языком с большой долей статического связывания. Даже там, где возможно легко реализовать введение динамических компонент (например, создание локальных массивов изменяемой размерности), это исключается ради поддержания единообразия;
· почти все случаи динамического связывания реализуются явно и требуют программной (технологической) поддержки программистом.
Именно поэтому примеры динамического связывания можно «перечесть по пальцам»:
· динамические переменные и массивы ( 5.6);
· динамическое связывание функций при помощи указателей на функции ( 9.3);
Модульное программирование
Что такое модульное программирование
Модуль в программировании — это фрагмент кода, имеющий определенное функциональное значение и характеризующийся логической завершенностью.
Модульное программирование — это способ создания программы посредством объединения модулей в единую структуру.
Применение способа позволяет значительно повысить скорость разработки, обеспечить ее надежность, упростить тестирование. Модульное программирование удобно при групповой работе, так как каждый из участников процесса имеет возможность сконцентрироваться только на своем объеме работ, не отвлекаясь на деятельность коллег.
Осторожно! Если преподаватель обнаружит плагиат в работе, не избежать крупных проблем (вплоть до отчисления). Если нет возможности написать самому, закажите тут.
Важное преимущество подхода — возможность заменить или обновить один из компонентов без изменений всей остальной системы.
В чем состоит особенность, основные принципы
Основной принцип метода сформулирован канадским программистом Дэвидом Парнасом. Согласно его высказыванию, для создания одного модуля достаточно минимальных знаний о содержании других. Именно на основе этого принципа Парнас выдвинул концепцию сокрытия информации в программировании.
Сокрытие в программировании — это способ проектирования, предполагающий разграничение доступа разных частей продукта к внутренним компонентам друг друга.
Технологически процесс выглядит так:
В качестве структурных единиц могут выступать сервисы, классы, библиотеки функций, структуры данных и другие единицы, реализующие определенную функцию и способные предоставить интерфейс к ней.
Разновидности модулей
В информатике существуют дополнительные понятия модуля.
Модуль — это набор команд, который имеет свое обозначение и который можно вызвать по имени.
Модуль — это совокупность программных операторов, имеющая идентификатор и граничные компоненты.
Также принято выделять три разновидности:
Примерами крупных могут послужить набор пакетов в языках Java, Ada, а также набор модулей в программном языке Modula-2.
Проблемы модульного программирования
Несмотря на существенные достоинства метода, специалисты подмечают и ряд недостатков:
Если первые три пункта легко нивелируются мощью современной техники, то последний целиком и полностью зависит от программистов, оптимальности предложенных ими решений.