|
© 2025
Александр Легалов
Содержание
Шаг третий. Добавление функциональности
Добавление новых новых функций, расширяющих разрабатываемую или сопровождаемую программу, также встречается достаточно часто. И здесь использование различных подходов имеет свои особенности. Их проявление опять-таки интереснее рассмотреть для случая, связанного с расширением функциональности альтернативных конструкций. В рамках третьего шага будем расширять код, написанный также на первом шаге, путем добавления в него функции, вычисляющей периметры разработанных фигур.
Вычисление периметра при процедурном подходе
Вычисление переметра при процедурном подходе выполняется отдельными функциями как для основ специализаций, так и для обобщения. При этом в обобщенной функции осуществляется централизованный выбор одного из обработчика специализации. Также отдельно добавляется функция, осуществляющая обход элементов контейнера в выводом значений периметров, расположенных в нем фигур.
// rectangle-perimeter.c - вычисление периметра прямоугольника
double RectanglePerimeter(Rectangle *r) {
return (double)(2*(r->x + r->y));
}
// triangle-perimeter.c - вычисление периметра треугольника
double TrianglePerimeter(Triangle *t) {
return (double)(t->a + t->b + t->c);
}
// figure-perimeter.c - вычисление периметра для текущей фигуры
double FigurePerimeter(Figure *s) {
switch(s->k) {
case RECTANGLE:
return RectanglePerimeter(&(s->r));
case TRIANGLE:
return TrianglePerimeter(&(s->t));
default:
return 0.0;
}
}
// Вывод содержимого контейнера в указанный поток
void ContainerPerimeterOut(Container *c, FILE* ofst) {
fprintf(ofst, "Perimeters of figures:\n");
for(int i = 0; i < c->len; i++) {
fprintf(ofst, "%d: %f\n" , i, FigurePerimeter(c->cont[i]));
}
}
// container-perimeter-out.c - вывод периметро из контейнера
void ContainerPerimeterOut(Container *c, FILE* ofst) {
fprintf(ofst, "Perimeters of figures:\n");
for(int i = 0; i < c->len; i++) {
fprintf(ofst, "%d: %f\n" , i, FigurePerimeter(c->cont[i]));
}
}
Следует также отметить, что добавление всех новых функций осуществляется без изменения ранее написанного кода. Изменяется только клиент, в который необходимо добавить вызов функции вывода периметров фигур, размещенных в контейнере.
...
ContainerPerimeterOut(&c, ofst);
...
Отсутствие изменений в ранее написанном коде также отражено в схеме зависимостей между обобщением и его специализациями, так как разрабатываемые функции не связаны с такими ограничениями, как интерфейсы или методы классов. Добавление только новых функций не завязано на полиморфизм. Поэтому централизованная обработка альтернатив в данной ситуации не играет роли.
Вычисление периметра при объектно-ориентированном подходе
Расширение ОО программы в данном случае связано с добавлением нового метода. Так как методы размещены внутри классов, это ведет к изменению их интерфейсов. В языке программирования C++ это не влияет на реализацию методов, которая осуществляется в отдельных единицах компиляции. В целом прямое решения этой задачи связано с добавлением в классы методов вычисления периметра и выглядит следующим образом.
// figure.h - класс, обобщающающий фигуры.
class Figure {
public:
// иденитификация, порождение и ввод фигуры из потока
virtual void In(std::ifstream &ifst) = 0; // ввод данных из потока
virtual void Out(std::ofstream &ofst) = 0; // вывод данных в файл
virtual void Out() = 0; // вывод данных в консоль
virtual double Perimeter() = 0; // вычисление периметра
};
// rectangle.h - прямоугольник
class Rectangle: public Figure {
int x, y; // ширина, высота
public:
// переопределяем интерфейс класса
virtual void In(std::ifstream &ifst); // ввод данных из потока
virtual void Out(std::ofstream &ofst); // вывод данных в файл
virtual void Out(); // вывод данных в cout
virtual double Perimeter(); // вычисление периметра
Rectangle(): x{0}, y{0} {} // создание без инициализации.
};
// triangle.h - треугольник
class Triangle: public Figure {
int a, b, c; // стороны
public:
// переопределяем интерфейс класса
virtual void In(std::ifstream &ifst); // ввод данных из потока
virtual void Out(std::ofstream &ofst); // вывод данных в файл
virtual void Out(); // вывод данных в cout
virtual double Perimeter(); // вычисление периметра
Triangle(): a{0}, b{0}, c{0} {} // создание без инициализации.
};
// container.h - контейнер фигур
class Container {
enum {max_len = 100}; // максимальная длина
int len; // текущая длина
Figure *cont[max_len];
public:
void In(std::ifstream &ifst); // ввод фигур в котнейнер из входного потока
void Out(std::ofstream &ofst); // вывод данных в файл
void Out(); // вывод данных в cout
void PerimeterOut(std::ofstream &ofst); // вывод периметров в файл
void PerimeterOut(); // вывод периметров в cout
void Clear(); // очистка контейнера от фигур
Container(); // инициализация контейнера
~Container() {Clear();} // утилизация контейнера перед уничтожением
};
В отдельных единицах компиляции описываются реализации методов, осуществляющих вычисление и вывод периметров.
// rectangle-preimeter.cpp - вычисление периметра прямоугольника
double Rectangle::Perimeter() {
return (double)(2*(x + y));
}
// triangle-perimeter.cpp - вычисление периметра треугольника
double Triangle::Perimeter() {
return (double)(a + b + c);
}
// container-perimeter-out.cpp - вывод периметров фигур
void Container::PerimeterOut(std::ofstream &ofst) {
ofst << "Perimeters of figures:\n";
for(int i = 0; i < len; i++) {
ofst << i << ": " << cont[i]->Perimeter() << "\n";
}
}
Изменение клиента, как и в процедурной программе, обусловлено необходимостью сформировать в нем вычисление периметров фигур.
...
c.PerimeterOut(ofst);
...
Модификации, связанные с добавление новой функции, представлены на схеме. С одной стороны они небольшие. С другой ведут к необходимостью перекомпиляции всех модулей, связанных с соответствующими классами.
Следует отметить, что в более сложных программных системах для нейтрализации подобных изменений предлагается разделение единственной ответственности (S) с обрамлением фасадом, разделение интерфейсов (I) адаптерами и прочие SOLID, а также другие танцы с бубнами. Но в данном случае рассматриваются только прямые решения, а не высокие материи.
Вычисление периметра при процедурно-параметрическом подходе
ПП подход, аналогично процедурному, обеспечивает безболезненное добавление новых функций. При этом, как и в ОО подходе, можно использовать динамический полиморфизм, обеспечивающий более гибкое расширение при возможных последующих шагах без упомянутых танцев. Описание вычислений периметров в функциях для основ специализаций, в обобщенной функции, в оработчиках специализаций, а также вывод периметров фигур, размещенных в контейнере, выглядят следующим образом.
// rectangle-perimeter.c - вычисление периметра прямоугольника
double RectanglePerimeter(Rectangle *r) {
return (double)(2*(r->x + r->y));
}
// triangle-perimeter.c - вычисление периметра треугольника
double TrianglePerimeter(Triangle *t) {
return (double)(t->a + t->b + t->c);
}
// figure-perimeter.c - обобщенная функция
double FigurePerimeter<Figure *f>() {return 0.0;} //= 0;
// figure-rectangle-perimeter.c - обработчик фигуры-прямоугольника
double FigurePerimeter<Figure.rect *f>() {
return RectanglePerimeter(&(f->@));
}
// figure-triangle-perimeter.c - обработчик фигуры-треугольника
double FigurePerimeter<Figure.trian *f>() {
return TrianglePerimeter(&(f->@));
// return (double)(f->@a + f->@b + f->@c);
}
// Вывод содержимого контейнера в указанный поток
void ContainerPerimeterOut(Container *c, FILE* ofst) {
fprintf(ofst, "Perimeters of figures:\n");
for(int i = 0; i < c->len; i++) {
fprintf(ofst, "%d: %f\n" , i, FigurePerimeter<c->cont[i]>());
}
}
Как и в предыдущих случаях, в клиентский код добавлена функция вывода периметров фигур, размещенных в контейнере.
...
ContainerPerimeterOut(&c, ofst);
...
Добавление новой функции не ведет ни к каким изменения в коде, ранее реализованном на первом шаге, что и отражено на схеме.
Выводы
В случает ОО подхода добавление новой функциональности при прямом решении ведет к изменению классов или интерфейсов. Существуют различные приемы, которые в ряде случаев используются для получения более гибких ОО решений. Проявляется ортогональность процедурного и ОО подходов. Для процедурных решений функциональность добавляется безболезненно, но происходит изменение кода при добавлении новых альтернатив. В случае ОО подхода наблюдается противоположная ситуация. ППП позволяет безболезненно решать задачи связанные как с добавлением новых альтернатив, так и новых функций, обеспечивающих обработку этих альтернатив, используя в обоих случаях достаточно простые и понятные решения.
Содержание
|