Модульное программирование предполагает группировку всех данных одного типа вокруг одного модуля, управляющего этим типом. Если потребуются стеки двух разных видов, можно определить управляющий ими модуль с таким интерфейсом:
struct stack_id { /* ... */ }; // stack_id - это дескриптор объекта
stack_id create_stack(int size); // создать стек и возвратить
// его идентификатор
void push(stack_id, char);
char pop(stack_id);
destroy_stack(stack_id); // уничтожение стека
Конечно такое решение намного лучше, чем хаос, свойственный традиционным, неструктурированным решениям, но моделируемые таким способом типы совершенно очевидно отличаются от "настоящих", встроенных. Каждый управляющий типом модуль должен определять свой собственный алгоритм создания "переменных" этого типа. Не существует универсальных правил присваивания идентификаторов, обозначающих объекты такого типа. У "переменных" таких типов не существует имен, которые были бы известны транслятору или другим системным программам, и эти "переменные" не подчиняются обычным правилам областей видимости и передачи параметров.
Тип, реализуемый управляющим им модулем, по многим важным аспектам существенно отличается от встроенных типов. Такие типы не получают той поддержки со стороны транслятора (разного вида контроль), которая обеспечивается для встроенных типов. Проблема здесь в том, что программа формулируется в терминах небольших (одно-два слова) дескрипторов объектов, а не в терминах самих объектов (stack_id может служить примером такого дескриптора).
В языках Ада, C, С++ и подобных им эта трудность преодолевается благодаря тому, что пользователю разрешается определять свои типы, которые трактуются в языке практически так же, как встроенные. Такие типы обычно называют абстрактными типами данных, хотя лучше, пожалуй, их называть просто пользовательскими. Парадигму же программирования можно выразить теперь так:
Определите, какие типы вам нужны; предоставьте полный набор операций для каждого типа.
Абстрактный тип данных определяется как некий "черный ящик". После своего определения он, по сути, никак не взаимодействует с программой. Его никак нельзя приспособить для новых целей, не меняя определения. В этом смысле это негибкое решение. Пусть, например, нужно определить для графической системы тип shape (фигура). Пока считаем, что в системе могут быть такие фигуры: окружность (circle), треугольник (triangle) и квадрат (square). Пусть уже есть определения точки и цвета:
class point { /* ... */ };
class color { /* ... */ };
Тип shape можно определить следующим образом:
enum kind {circle, triangle, square};
class shape
{
point center;
color col;
kind k;
public:
point where() {return center;}
void move(point to) {center = to; draw();}
void draw();
void rotate(int);
// еще некоторые операции
};
"Поле типа" k необходимо для того, чтобы такие операции, как draw() и rotate(), могли определять, с какой фигурой они имеют дело. Функцию draw() можно определить так:
void shape :: draw()
{
switch ( k )
{
case circle:
// рисование окружности
break;
case triangle:
// рисование треугольника
break;
case square:
// рисование квадрата
break;
}
}
Это не функция, а кошмар. В ней нужно учесть все возможные фигуры, какие только есть. Поэтому она дополняется новыми операторами, как только в системе появляется новая фигура. Плохо то, что после определения новой фигуры нужно проверить и, возможно, изменить все старые операции класса.