[ <<< | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Источники | >>> ]


© 2001 А.И. Легалов

Вариантное обобщение

Обозначим термином "вариант" основу обобщения данных в процедурном подходе. Обобщение, применяемое в процедурном подходе и построенное на основе варианта, назовем вариантным обобщением. С каждым вариантом связан набор специализаций обобщения, построенный на основе уже существующих абстракций. Определим их как вариантные специализации.

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

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

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

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

Построение вариантного обобщения на основе общего ресурса

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

//----------------------------------------------------------------

// прямоугольник
struct rectangle {
    int x, y; // ширина, высота
};

//----------------------------------------------------------------

// треугольник
struct triangle {
    int a, b, c; // стороны
};

//----------------------------------------------------------------

С каждой из специализаций связывается следующий набор обработчиков:

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

//----------------------------------------------------------------
// Процедуры, обеспечивающие работу с прямоугольником
//----------------------------------------------------------------

// Динамическое создание прямоугольника по двум сторонам
rectangle *Create_rectangle(int x, int y)
{
    rectangle *r = new rectangle;
    r->x = x; 
    r->y = y;
    return r;
}

//----------------------------------------------------------------

// Инициализация уже созданного прямоугольника по двум сторонам
void Init(rectangle &r, int x, int y)
{
    r.x = x; 
    r.y = y;
}

//----------------------------------------------------------------

// Ввод параметров прямоугольника
void In(rectangle &r) 
{
    cout << "Input Rectangle: x, y = ";
    cin >> r.x >> r.y;
}

//----------------------------------------------------------------

// Вывод параметров прямоугольника
void Out(rectangle &r) 
{
    cout << "It is Rectangle: x = "
         << r.x << ", y = " 
         << r.y << endl;
}

//----------------------------------------------------------------

// Вычисление площади прямоугольника
double Area(rectangle &r) 
{
    return r.x * r.y;
}

//----------------------------------------------------------------
// Процедуры, обеспечивающие работу с треугольником
//----------------------------------------------------------------

// Динамическое создание треугольника по трем сторонам
triangle *Create_triangle(int a, int b, int c)
{
    triangle *t = new triangle;
    t->a = a; 
    t->b = b;
    t->c = c;
    return t;
}

//----------------------------------------------------------------

// Инициализация уже созданного треугольника по трем сторонам
void Init(triangle &t, int a, int b, int c)
{
    t.a = a; 
    t.b = b; 
    t.c = c; 
}

//----------------------------------------------------------------

// Ввод параметров треугольника
void In(triangle &t) 
{
    cout << "Input Triangle: a, b, c = ";
    cin >> t.a >> t.b >> t.c;
}

//----------------------------------------------------------------

// Вывод параметров треугольника
void Out(triangle &t)
{
    cout << "It is Triangle: a = " 
      << t.a << ", b = " << t.b
      << ", c = " << t.c << endl;
}

//----------------------------------------------------------------

// Вычисление площади треугольника
double Area(triangle &t)
{
    double p = (t.a + t.b + t.c) / 2.0; // полупериметр
    return sqrt(p * (p-t.a) * (p-t.b) * (p-t.c));
}

//----------------------------------------------------------------

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

// Обобщение на основе разделяемого (общего) ресурса
union { 
    rectangle r;
    triangle t;
};

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

//----------------------------------------------------------------

// структура, обобщающая все имеющиеся фигуры
  struct shape {
    // значения ключей для каждой из фигур
    enum key {RECTANGLE, TRIANGLE};
    key k; // ключ
    // Обобщение на основе разделяемого (общего) ресурса
    union { // используем простейшую реализацию
      rectangle r;
      triangle t;
  };
};

//----------------------------------------------------------------

Вариантные процедуры, работающие с построенным обобщением, осуществляют окончательное объединение обработчиков специализаций:

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

//----------------------------------------------------------------
// Процедуры, обеспечивающие работу с обобщенной фигурой
//----------------------------------------------------------------

// Сигнатуры, необходимые обработчикам вариантов.
void Init(rectangle &r, int x, int y);
void Init(triangle &t, int a, int b, int c);
void In(rectangle &r);
void In(triangle &t);
void Out(rectangle &r);
void Out(triangle &t);
double Area(rectangle &r);
double Area(triangle &t);

//----------------------------------------------------------------

// Динамическое создание обобщенного прямоугольника
shape *Create_shape_rectangle(int x, int y)
{
    shape *s = new shape;
    s->k = shape::key::RECTANGLE;
    Init(s->r, x, y);
    return s;
}

//----------------------------------------------------------------

// Инициализация обобщенного прямоугольника
void Init_rectangle(shape &s, int x, int y)
{
    s.k = shape::key::RECTANGLE;
    Init(s.r, x, y);
}

//----------------------------------------------------------------

// Динамическое создание обобщенного треугольника
shape *Create_shape_triangle(int a, int b, int c)
{
    shape *s = new shape;
    s->k = shape::key::TRIANGLE;
    Init(s->t, a, b, c);
    return s;
}

//----------------------------------------------------------------

// Инициализация обобщенного треугольника
void Init_triangle(shape &s, int a, int b, int c)
{
    s.k = shape::key::TRIANGLE;
    Init(s.t, a, b, c);
}

//----------------------------------------------------------------

// Ввод параметров обобщенной фигуры из стандартного потока ввода
shape* In()
{
    shape *sp;
    cout << "Input key: for Rectangle is 1, "
            "for Triangle is 2, else break: ";
    int k;
    cin >> k;
    switch(k) {
    case 1:
      sp = new shape;
      sp->k = shape::key::RECTANGLE;
      In(sp->r);
      return sp;
    case 2:
      sp = new shape;
      sp->k = shape::key::TRIANGLE;
      In(sp->t);
      return sp;
    default:
      return 0;
    }
  }

//----------------------------------------------------------------

// Вывод параметров текущей фигуры в стандартный поток вывода
void Out(shape &s) 
{
    switch(s.k) {
    case shape::key::RECTANGLE:
      Out(s.r);
      break;
    case shape::key::TRIANGLE:
      Out(s.t);
      break;
    default:
      cout << "Incorrect figure!" << endl;
    }
}

//----------------------------------------------------------------

// Нахождение площади обобщенной фигуры
double Area(shape &s)
{
    switch(s.k) {
    case shape::key::RECTANGLE:
      return Area(s.r);
    case shape::key::TRIANGLE:
      return Area(s.t);
    default:
      return 0.0;
    }
}

//----------------------------------------------------------------

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

Существующие языки программирования по-разному поддерживают механизм формирования обобщения на основе общего ресурса. В языках C и C++ [Страуструп], для задания ключа приходится создавать дополнительную структуру. Языки программирования Pascal [Вирт85] и Modula-2 [Вирт87] позволяют сразу создавать вариантные записи с ключом, но контроль на соответствие между текущим значением ключа и хранимой специализацией отсутствует. В языке программирования Ada [Джехани] используются объединения, в которых значение ключа точно соответствует хранимому объекту и обеспечивает поддержку дополнительного контроля при доступе к объекту во время выполнения. Это позволяет сгенерировать исключение при попытке некорректно использовать экземпляр обобщения.

Исходный текст программы, использующей обобщение на основе общего ресурса, приведен в уже упомянутом архиве pp_examp1.zip.

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

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

Построение вариантного обобщения на основе альтернативного связывания

Использование альтернативного связывания позволяет, зачастую, обойтись без дополнительных обобщающих конструкций, что обеспечивает большую гибкость при эволюционном наращивании процедурной программы. В этом плане можно посмотреть, как изменится метод решения задачи, если использование общего ресурса заменить вариантным связыванием. Сразу отмечу, что эквивалентных вариантов реализации существует очень много, а проводить их сравнение и анализ вряд ли имеет смысл (что получилось, то и показываю:). Сохраним существующие специализации без изменения, но заменим обобщение:

//----------------------------------------------------------------

// структура, обобщающая все имеющиеся фигуры
  struct shape {
    // значения ключей для каждой из фигур
    enum key {RECTANGLE, TRIANGLE};
    key k; // ключ
    // используемые альтернативные указатели
    union { // используем простейшую реализацию
      rectangle *pr;
      triangle *pt;
    };
  };

//----------------------------------------------------------------

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

Следует также отметить, что гибкость механизма альтернативного связывания привела к его непосредственной, явной или опциональной, поддержке в ряде языков программирования. В языке программирования C++ реализован механизм идентификации типов во время выполнения (RTTI) [Страуструп]. Он включается опционально, что позволяет использовать абстрактные типы как с дополнительными полями, поддерживающими динамическую типизацию, так и без них. В языке программирования Оберон такая поддержка введена явно. При этом в языке отсутствуют вариантные записи, обеспечивающие поддержку наложения ресурсов. Вирт [Wirth] посчитал, что такой механизм является избыточным. Возможно, что он прав, когда дело касается языка, не ориентированного на серьезное (машиннозависимое) системное программирование (да, я знаю, что на Обероне написана его же операционная система и оболочка для работы с пользователем:).

Кстати, я отношу использование RTTI к процедурному программированию, что определяется его идеологией, связанной с поддержкой признака объекта на уровне языковой семантики и использованием данного признака при централизованном выборе альтернатив. Как не крути, но использование контроля типа во время выполнения поднимает те же проблемы эволюционного развития программы, что и процедурный подход. При этом не важно, каким образом реализовано получение информации о типе объекта. Она может быть получена через специальную функцию (например, typeid в С++ [Страуструп]) или дополнительную переменную, параметризирующую объект (как это сделано в языке Оберон-2 [MoessenboeckWirth]).

Не привожу в качестве примера другие языки, так как данный механизм в большинстве из них - это не только дань моде. Это способ преодоления недостатков, присущих "чистому" объектно-ориентированному подходу! До этого момента мы еще доберемся, но уже сейчас можно сказать, что "чистых" ОО языков (без RTTI) практически не существует из-за низкой эффективности однорукого объектного программирования!

Построение вариантного обобщения на основе образного восприятия

Вряд ли, в наше время, имеет смысл писать пример, демонстрирующий образное восприятие обобщений, в стиле языков Фортран или Алгол-60. Такое сейчас редко увидишь даже в нормальных программах, написанных на Ассемблере (и туда уже дошли механизмы описания не только абстрактных типов данных, но и классов). Однако, достаточно часто встречаются приемы, обеспечивающие сочетания образного восприятия с другими методами формирования вариантов. Это делается как по незнанию, так и для получения кода, обладающего меньшей зависимостью от других частей. Следует отметить, что в последнем случае часто снижается семантический контроль программы во время компиляции. Ловите ошибки во время выполнения!

Для иллюстрации образного восприятия вариантов, введем в нашу программу следующие изменения:

Результаты таких умозрительных рассуждений отображены в следующих структурах контейнера и обобщений.

//----------------------------------------------------------------

struct container
{
    enum {max_len = 100}; // максимальная длина
    int len; // текущая длина
    // Контейнер обазных указателей на специализации, построенные
    // на основе распределенного ключа
    void* cont[max_len];
};

//----------------------------------------------------------------

// Использование образного восприяитя обобщения на основе ключа,
// разнесенного по отдельным дополнительным специализациям

//----------------------------------------------------------------

// шаблонная структура, обобщающая все имеющиеся фигуры
struct shape {
    // значения ключей для каждой из фигур
    int k; 
    // образый ключ устанавливается для каждой вводимой фигуры. 
    // Можно легко ошибиться, но гибкость - прекрасная!
    // А связь с фигурами отсутствует!
    // Предполагается, что они пропишутся отдельно и будут связаны
    // через образное наложение одинаковых структур памяти
  };

//----------------------------------------------------------------

// структура, обобщающая прямоугольник
struct r_shape {
    // значения ключей для каждой из фигур
    int k; // образый ключ  = 1
    // А здесь будет храниться прямоугольник
    rectangle r;
};

//----------------------------------------------------------------

// структура, обобщающая треугольнк
struct t_shape {
    // значения ключей для каждой из фигур
    int k; // образый ключ  = 2
    // А здесь будет храниться треугольник
    triangle t;
};

//----------------------------------------------------------------

Следует отметить ряд особенностей. Контейнер теперь совершенно не зависит от хранимых в нем фигур и может использоваться для чего угодно. Пользуйтесь и ошибайтесь на здоровье! Каждая из геометрических фигур использует дополнительный ключ, который тоже не контролируется. Структура shape введена для того, чтобы выделить общую часть всех обобщающих фигур (в данном случае - ключ). Она используется в качестве шаблона при анализе ключей экземпляров специализаций.

Использование "всеохватывающего" типа void ведет к потере информации о подключаемом типе, то есть, "растипизации". Это не позволяет в дальнейшем использовать объект без явного и неконтролируемого приведения типов, осуществляемого во всех обработчиках вариантов после анализа текущего значения признака. Надо хорошо постараться, чтобы учесть возможные преобразования и обеспечить их правильность. В остальном же процесс обработки альтернатив мало чем отличается от тех методов, которые использовались при построении обобщений на основе общего ресурса и альтернативного связывания. Текст данной версии программы расположен в архиве pp_examp1c.zip.

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


[ <<< | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Источники | >>> ]