Модульность

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

В объектно-ориентиро­ванном программировании по модулям необходимо распре­делить классы и объекты.

В большинстве языков, поддерживающих принцип модульности, ин­терфейс модуля отделен от его реализации. Таким образом, принципы модульности и ин­капсуляции являются взаимосвязанными.

В языке C++ модулями являются файлы, которые компилируются отдельно один от другого и затем объединяются в один исполняемый файл при помощи редактора связей.

Пример. В качестве примера рассмотрим модульную структуру программы, использующей стек.

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

Как правило, объявления, описывающие интерфейс модуля, помещаются в так называемый заголовочный файл, имеющий характерное имя, которое отражает его использование. Заголовочный файл обычно включается и в файл с пользовательским кодом, и в файл с реализацией модуля. Это достаточно простой и эффективный способ обеспечить идентичность представления интерфейса в обоих файлах. Включение информации об интерфейсе в файл с пользовательским кодом обусловлено необходимостью проверки типов используемых в нем интерфейсных функций во время компиляции.

Итак, интерфейс стека будет помещен в файл stack.h.

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

void push(int el);

int pop();

Код пользователя будет находиться, например, в файле user.cpp:

#include "stack.h" // включить интерфейс

main(void)

{

push(1);

if (pop()!= 1)...; //???

...

}

Файл, содержащий реализацию модуля Stack, может называться, например, stack.cpp:

#include "stack.h" // включить интерфейс

int stack [100]; //реализация

int top;

void push(int el){...}

int pop(){...}

Тексты user.cpp и stack.cpp совместно используют информацию об интерфейсе, содержащуюся в stack.h. Во всем другом эти два файла независимы и могут быть раздельно откомпилированы. Графи­ческое изображение упомянутых фрагментов программы представлено на рис. 2.1.

Рис. 2.1 Структура модулей программы, использующей стек

Если стек представлен в виде объекта типа Stack, введенного с использованием понятия класс, то информация о данных, агрегированных в этот новый тип, (а не только о предоставляемом им интерфейсе) также должна быть доступна при компиляции пользовательского кода на языке С++.

Предположим, что компилятор встречает объявление объекта

Stack My_stack;

Компилятор должен знать, сколько отвести под него памяти. Если бы эта информация содержалась только в реализации класса, нам пришлось бы написать ее полностью, прежде чем мы смогли бы задействовать клиентов класса. То есть весь смысл отделения интерфейса от реализации был бы потерян.

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

В результате файл stack.h должен содержать описание структуры или класса Stack, приведенное в разд. 2.2.

Файл stack.cpp, содержащий реализацию стека, имеет вид

#include "stack.h"

void Stack:: push(const int el) { …}

int Stack:: pop() { …}

bool Stack:: isFull() const { …}

bool Stack:: isEmpty() const { …}

Формат записи функций здесь включает двойное двоеточие (::) – оператор разрешения области видимости. С его помощью формируется квалифицированное имя члена класса

имя_класса:: имя_члена_класса

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

Заметим, что язык С++ позволяет включить реализацию функций-членов в описание класса

class String {

int length;

public:

int getLength(void) const { return length; };

};

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

Например, фрагмент

String s;

int x = s.getLength();

в случае подстановки функции выполняется, как будто он был записан как

String s;

int x = s.length;

Подстановка осуществляется только для относительно простых коротких функций. Имеется ряд ограничений для применения подстановки.

При записи реализации функции вне описания класса можно указать свойство подстановки, используя ключевое слово inline.

inline int String:: getLength(void) const { return length; };

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

Рассмотрим приемы и правила, которые позволяют составлять модули наиболее эффективным обра­зом:

– конечной целью разбиения программы на модули является снижение затрат на программирование за счет независимой раз­работки и тестирования;

– структура модуля должна быть достаточно простой для восприятия;

– реализация каждого модуля не должна зависеть от реализации других модулей;

– должны быть приняты меры для облегчения процесса внесения измене­ний там, где они наиболее вероятны.

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

– особенности системы, подвер­женные изменениям, следует скрывать в отдельных модулях;

– в качестве межмо­дульных можно использовать только те элементы, вероятность изменения кото­рых мала;

– все структуры данных должны быть обособлены в модуле; доступ к ним будет возможен для всех процедур этого модуля и закрыт для всех других;

– доступ к данным из модуля должен осуществляться только через процедуры данного мо­дуля.

Следует стремиться построить модули так, чтобы объединить логически связанные абстракции и минимизировать взаимные связи между модулями.

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

Могут сказываться и требования секретности: одна часть кода может быть несекретной, а другая – секретной, тогда последняя выполняется в виде отдельного модуля (модулей).

В результате всего сказанного сформулируем следующее определение модульности.

Модульность – это свойство системы, разложенной на цельные, но слабо связанные между собой модули.

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

Подсисте­ма – это агрегат, содержащий другие модули и другие подсистемы. Каждый модуль в системе должен располагаться в одной подсистеме или находиться на самом верхнем уровне.

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


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: