SoftCraft
разноликое программирование

Top.Mail.Ru

Динамический полиморфизм и эволюция

© 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);
  ...

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

Шаг 3. Добавление функциональности при процедурном подходе

Вычисление периметра при объектно-ориентированном подходе

Расширение ОО программы в данном случае связано с добавлением нового метода. Так как методы размещены внутри классов, это ведет к изменению их интерфейсов. В языке программирования 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);
  ...

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

Шаг 3. Добавление функциональности при ОО подходе

Следует отметить, что в более сложных программных системах для нейтрализации подобных изменений предлагается разделение единственной ответственности (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);
  ...

Добавление новой функции не ведет ни к каким изменения в коде, ранее реализованном на первом шаге, что и отражено на схеме.

Шаг 3. Добавление функциональности при ПП подходе

Выводы

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


Содержание