|
© 2025
Александр Легалов
Содержание
Шаг восьмой. Добавление обобщения, использующего существующие специализации
Существуют ситуации, когда один и тот же программный объект может принадлежать нескольким альтернативным категориям. То есть, речь идет о принадлежности некоторой конструкции одновременно нескольким обобщениям. В качестве примера можно привести животных, которые могут делиться на различные классы, быть плавающими или летающими. И еще много какими. Не мудурствуя, придумаем для наших геометрических фигур еще одно обобщение, которое содержит общую переменную, значение которой равно периметру фигуры. При этом сами фигуры принадлежат как к новой категории, так и к уже имеющейся. Для хранения и идентификации новых специализаций введем также контейнер для фигур с периметрами.
Добавление обобщения в процедурную программу
Вновь вводимое обобщение, по аналогии с уже существующим, будет представлено в виде структуры, содержащей объединения уже имеющихся фигур. При дальнейшем построении кода от первого шага такими фигурами будут прямоугольник и треугольник.
// new-generalization.h
typedef struct FigurePerimeter {
double perimeter;
key k;
union {
Rectangle* r;
Triangle* t;
};
} FigurePerimeter;
enum {max_len_p = 100};
// Простейший контейнер на основе одномерного массива
typedef struct ContainerOfPerimeters {
int len; // текущая длина
struct FigurePerimeter cont[max_len_p];
} ContainerOfPerimeters;
Структура содержит поле для хранения периметра. Для того, чтобы можно было подключаться к основам специализаций уже существующих фигур, объединение содержит указатели на основы специализаций. Так как объединение содержит только альтернативные указатели, то размер обобщения будет одинаков независимо от подключаемых альтернатив. Поэтому контейнер для хранения новых специализаций содержит массив обобщенных структур вместо указателей на них.
Использование нового обобщения и его специализаций заключается в заполнении массива фигур с периметрами, где каждая новая специализация содержит признак фигуры, вычисленный периметры и через указатель связывается с той фигурой признак которой соответствует ключу исходной специализации. Наряду с заполнением нового контейнера, также реализуются и функции вывода его содержимого.
// new-generalization.c
// Формирование конкретной специализации дополнительного обобщения на основе
// известной специализации фигуры. Область памяти под фигуру имеется
void SelectNewPerimeterSpecFromFigure(Figure* f, FigurePerimeter* fp) {
switch(f->k) {
case RECTANGLE:
fp->perimeter = RectanglePerimeter(&(f->r));
fp->r = &(f->r);
break;
case TRIANGLE:
fp->perimeter = TrianglePerimeter(&(f->t));
fp->t = &(f->t);
break;
default:
printf("SelectNewPerimeterSpecFromFigure: Incorrect Figure\n");
exit(15);
}
fp->k = f->k;
}
//------------------------------------------------------------------------------
// Добавление основы специализации в массив, состоящий из специализаций
// дополнительного обобщения.
void AddNewGeneralization(ContainerOfPerimeters* fpc, Container* c) {
fpc->len = c->len;
for(int i = 0; i < c->len; ++i) {
SelectNewPerimeterSpecFromFigure(c->cont[i], &fpc->cont[i]);
}
}
//==============================================================================
// Организация вывода специализаций нового обобщения и вывод содержимог
// контейнера с новыми специализациями периметров фигур
//==============================================================================
// Функция вывода значения периметра из поля обобщения
void NewFigurePerimeterOut(FigurePerimeter* fp, FILE* ofst) {
fprintf(ofst, "Perimeter = %lf. ", fp->perimeter);
switch(fp->k) {
case RECTANGLE:
RectangleOut(fp->r, ofst);
break;
case TRIANGLE:
TriangleOut(fp->t, ofst);
break;
}
}
//------------------------------------------------------------------------------
// Вывод содержимого контейнера периметров фигур в указанный поток
void ContainerNewPerimeterOut(ContainerOfPerimeters* fpc, FILE* ofst) {
fprintf(ofst, "Container contains %d elements.\n", fpc->len);
for(int i = 0; i < fpc->len; i++) {
fprintf(ofst, "%d: " , i);
NewFigurePerimeterOut(&(fpc->cont[i]), ofst);
}
}
Для вычисления периметра в программе используются функции разработанные на шаге 3, когда требовалось добавить новую функциональность. Они используются для заполнения полей периметров в новых специализациях. Добавление данных в новый контейнер осуществляет при обходе уже сформированного контейнера с получением в нем адресов основ специализаций и подключению этих основ к соответствующим указателям (функция AddNewGeneralization).
Клиентский код расширяется за счет использование в нем нового контейнера, его заполнения и вывода.
// New generalization container
ContainerOfPerimeters pc;
AddNewGeneralization(&pc, &c);
ContainerNewPerimeterOut(&pc, ofst);
Схема показывает, что добавление нового обобщения при процедурном подходе не меняет ранее написанный код. При этом старые основы специализаций связываются посредством указателей, что обеспечивает доступ к ним через оба обобщения.
Добавление обобщения в объектно-ориентированную программу
Добавление новых обобщений в уже существующую OO программу является более трудоемким. Это связано с отсутствием явно выделенных основ специализаций, так как использование наследования от базового класса обеспечивает сразу же создание специализаций. Поэтому в случае прямого решения в новом обобщении приходится подключать специализации, построенные от предыдущего обобщения.
Ситуацию можно упростить, если привести рефакторинг программы, сформированной на первом шаге. Создав отдельные классы для прямоугольника и треугольника, а затем, в соответствии с современными рекомендациями построить первоначальную категорию фигур с использованием композиций. Но в данном случае продолжим уже имеющееся решения и постараемся обойтись без дополнительных трюков.
Для этого сформируем фигуру с периметром, а от нее построим пару производных классов, подключив ранее созданные классы прямоугольника и треугольника через указатели. Контейнер для хранения элементов нового обобщения практически аналогичен ранее созданному.
// new-generalization.h
// Класс, хранящий периметр фигуры
class FigurePerimeter {
protected:
double perimeter;
// Конкретные фигуры прицепляются к производным классам
public:
virtual void Out(std::ofstream &ofst) = 0; // вывод данных в файл
// вычисление периметра по параметрам подключаемой фигуры
void SelectNewPerimeterFromFigure(Figure* f);
};
// Производный класс, расчитанный на подключение прямоугольников
class FigurePerimeterRectangle: public FigurePerimeter {
// Связь только с прямоугольником
Rectangle *rect;
public:
virtual void Out(std::ofstream &ofst);
void FieldPerimeterOut(std::ofstream &ofst);
FigurePerimeterRectangle(Rectangle* r): rect{r} {}
};
// Производный класс, расчитанный на подключение треугольников
class FigurePerimeterTriangle: public FigurePerimeter {
// Связь только с треугольником
Triangle *trian;
public:
virtual void Out(std::ofstream &ofst);
void FieldPerimeterOut(std::ofstream &ofst);
FigurePerimeterTriangle(Triangle* t): trian{t} {}
};
//------------------------------------------------------------------------------
// Простейший контейнер на основе одномерного массива
// для хранения данных нового обобщения
class ContainerOfPerimeters {
enum {max_len_p = 100}; // максимальная длина
int len; // текущая длина
FigurePerimeter* cont[max_len_p];
public:
void AddNewGeneralization(Container* c);
void In(std::ifstream &ifst); // ввод фигур в котнейнер из входного потока
void Out(std::ofstream &ofst); // вывод фигур в файл
void Clear(); // очистка контейнера от фигур
ContainerOfPerimeters() {len = 0;} // инициализация контейнера
~ContainerOfPerimeters() {Clear();} // утилизация контейнера
};
Так как формирование нового обобщения, опирается на уже имеющиеся специализации и использование при этом композиции для их связывания, реализация также формируется безболезненно.
// new-generalization.cpp
// Вычисление периметра прямоугольника по параметрам подключаемой фигуры.
// Можно сделать не виртуальной, определяемой отдельно в каждой специализации,
// но не хочется этим заниматься на уровне примера.
void FigurePerimeter::SelectNewPerimeterFromFigure(Figure* f) {
perimeter = f->Perimeter();
}
// Добавление основы специализации в массив специализаций
// дополнительного обобщения.
void ContainerOfPerimeters::AddNewGeneralization(Container* c) {
len = c->GetLen();
FigurePerimeter* fp = nullptr;
for(int i = 0; i < len; ++i) {
Figure* f = c->GetFigure(i); // текущая фигура в исходном контейнере
FigurePerimeterRectangle* fpr;
FigurePerimeterTriangle* fpt;
// Создание специализации фигуры с периметром в зависимости от типа фигуры,
if(dynamic_cast<Rectangle*>(f) != nullptr) {
fpr = new FigurePerimeterRectangle(static_cast<Rectangle*>(f));
fpr->SelectNewPerimeterFromFigure(f);
cont[i] = fpr;
} else if(dynamic_cast<Triangle*>(f) != nullptr) {
fpt = new FigurePerimeterTriangle(static_cast<Triangle*>(f));
fpt->SelectNewPerimeterFromFigure(f);
cont[i] = fpt;
} else {
std::cerr << "Incorrect Figure\n";
exit(13);
}
}
}
//==============================================================================
// Вывод специализаций нового обобщения и содержимого
// контейнера с новыми специализациями периметров фигур
//==============================================================================
// Вывод значения периметра прямоугольника из поля обобщения
void FigurePerimeterRectangle::FieldPerimeterOut(std::ofstream &ofst) {
ofst << "Rectangle Perimeter = " << perimeter << ". " ;
}
// Вывода значения периметра треугольника из поля обобщения в файл
void FigurePerimeterTriangle::FieldPerimeterOut(std::ofstream &ofst) {
ofst << "Triangle Perimeter = " << perimeter << ". " ;
}
// Вывод прямоугольника с периметром
void FigurePerimeterRectangle::Out(std::ofstream &ofst) {
FieldPerimeterOut(ofst);
rect->Out(ofst);
}
// Вывод треугольника с периметром
void FigurePerimeterTriangle::Out(std::ofstream &ofst) {
FieldPerimeterOut(ofst);
trian->Out(ofst);
}
//------------------------------------------------------------------------------
// Вывод содержимого контейнера периметров фигур
void ContainerOfPerimeters::Out(std::ofstream &ofst) {
ofst << "Container contains " << len << " elements.\n";
for(int i = 0; i < len; i++) {
ofst << i << ": ";
cont[i]->Out(ofst);
}
}
// Очистка контейнера от элементов (освобождение памяти)
void ContainerOfPerimeters::Clear() {
for(int i = 0; i < len; i++) {
delete cont[i];
}
len = 0;
}
Как и в случае процедурной программы, клиент расширяется за счет подключения нового контейнера, а также методов его заполнения и вывода.
// New generalization container
ContainerOfPerimeters pc;
pc.AddNewGeneralization(&c);
ofst << "Second container.\n";
pc.Out(ofst);
Схема демонстрирует отсутствие прямого влияния на ранее написанный код и косвенное использование специализаций предыдущего обобщения. Хотя, конечно, существуют и другие варианты реализации расширения с добавлением новых обобщений.
Добавление обобщения в процедурно-параметрическую программу
Формирование нового обобщения в процедурно-параметрической программы для любых типов данных не вызывает никаких проблем, так как любые основы специализаций добавляются безболезненно. Поэтому просто добавим исходные прямоугольник и треугольник.
// new-generalization.h
// Новое обобщение
FigurePerimeter + <empty: void*;>;
FigurePerimeter + <rect: Rectangle*;>;
FigurePerimeter + <trian: Triangle*;>;
// Простейший контейнер на основе одномерного массива
enum {max_len_p = 100};
typedef struct ContainerOfPerimeters {
int len; // текущая длина
struct FigurePerimeter.empty cont[max_len_p];
} ContainerOfPerimeters;
Структура контейнера для нового обобщения также полностью повторяет уже имеющийся контейнер. Но есть один нюанс, связанный с демонстрацией другого приема. Так как используемые специализации имеют в качестве основы указатели, то их размер равен друг другу. Следовательно в контейнере можно хранить не указатели на обобщение, а непосредственно специализации, связанные со своими основами, что позволяет избавиться от лишнего динамического выделения памяти. Для первоначального заполнения контейнера используем дополнительную специализацию empty такого же размера.
Как и в предшествующих программах вычисление периметра заимствуется из третьего шага. Дополнительные функции обеспечивают вычисления периметров специализаций, подключение основ специализаций из уже существующего контейнера, заполнение нового контейнера и вывод его элементов.
// new-generalization.c
// Формирование конкретной специализации дополнительного обобщения
void SelectNewPerimeterSpecFromFigure<Figure* f>(FigurePerimeter* fp) {
printf("SelectNewPerimeterSpecFromFigure: Incorrect Figure\n");
exit(15);
}
void SelectNewPerimeterSpecFromFigure<Figure.rect* f>(FigurePerimeter* fp) {
init_spec(FigurePerimeter.rect, fp);
struct FigurePerimeter.rect* fpr = (struct FigurePerimeter.rect*)fp;
fpr->@ = &(f->@);
fpr->perimeter = RectanglePerimeter(&f->@);
}
void SelectNewPerimeterSpecFromFigure<Figure.trian* f>(FigurePerimeter* fp) {
init_spec(FigurePerimeter.trian, fp);
struct FigurePerimeter.trian* fpr = (struct FigurePerimeter.trian*)fp;
fpr->@ = &(f->@);
fpr->perimeter = TrianglePerimeter(&f->@);
}
// Добавление основы специализации в массив, состоящий из специализаций
// дополнительного обобщения.
void AddNewGeneralization(ContainerOfPerimeters* fpc, Container* c) {
fpc->len = c->len;
for(int i = 0; i < c->len; ++i) {
SelectNewPerimeterSpecFromFigure<c->cont[i]>
((FigurePerimeter*)&fpc->cont[i]);
}
}
//==============================================================================
// Организация вывода специализаций нового обобщения и вывод содержимого
// контейнера с новыми специализациями периметров фигур
//==============================================================================
// Функция вывода значения периметра из поля обобщения
void FieldPerimeterOut(FigurePerimeter* fp, FILE* ofst) {
fprintf(ofst, "Perimeter = %lf. ", fp->perimeter);
}
// Обобщающая функция для вывода периметров и параметров связанной фигуры
void NewFigurePerimeterOut<FigurePerimeter *fp>(FILE* ofst) {} //= 0;
// Вывод периметра и связанного с ним прямоугольника
void NewFigurePerimeterOut<FigurePerimeter.rect *fp>(FILE* ofst) {
FieldPerimeterOut((FigurePerimeter*)fp, ofst);
RectangleOut(fp->@, ofst);
}
// Вывод периметра и связанного с ним треугольника
void NewFigurePerimeterOut<FigurePerimeter.trian *fp>(FILE* ofst) {
FieldPerimeterOut((FigurePerimeter*)fp, ofst);
TriangleOut(fp->@, ofst);
}
//------------------------------------------------------------------------------
// Вывод содержимого контейнера периметров фигур
void ContainerNewPerimeterOut(ContainerOfPerimeters* fpc, FILE* ofst) {
fprintf(ofst, "Container contains %d elements.\n", fpc->len);
for(int i = 0; i < fpc->len; i++) {
fprintf(ofst, "%d: " , i);
NewFigurePerimeterOut<(FigurePerimeter*)&(fpc->cont[i])>(ofst);
}
}
Аналогично другим парадигмам в клиентский код добавляется создание и использования контейнера с новыми обобщениями.
// New generalization container
ContainerOfPerimeters pc;
AddNewGeneralization(&pc, &c);
ContainerNewPerimeterOut(&pc, stdout);
Добавление артефактов, определяющих формирование и использование нового обобщения, представлено на схеме. Как и в предшествующих ситуациях ранее написанный код не изменяется.
Выводы
Добавление нового обобщения в процедурные и ПП программы осуществляется без каких-либо проблем. Даже в тех случаях, когда косвенное связывание из нового обобщения требуется организовать внутрь монолитной структуры. В случае ОО подхода при прямой реализации приходится использовать специализации старого обобщения или изменять старое обобщение, реализуя связывание с основами специализаций посредством композиции.
Содержание
|