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

Top.Mail.Ru

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

© 2025
Александр Легалов


Содержание


Шаг второй. Добавление альтернативы

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

Круг и процедурный подход

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


  // circle.h - круг
  typedef struct Circle {
    int r; // радиус круга
  } Circle;

  // circle-in.c - ввод круга
  void CircleIn(Circle *c, FILE* ifst) {
    fscanf(ifst, "%d", &(c->r));
  }

  // circle-out.c - вывод круга
  void CircleOut(Circle *c, FILE *ofst) {
    fprintf(ofst, "It is Circle: r = %d\n", c->r);
  }

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

// figure.h - структура, обобщающая фигуры
// значения ключей для каждой из фигур
typedef enum key {RECTANGLE, TRIANGLE, CIRCLE} key;

// структура, обобщающая все имеющиеся фигуры
typedef struct Figure {
  key k; // ключ
  // используемые альтернативы
  union { // используем простейшую реализацию
    Rectangle r;
    Triangle t;
    Circle    c;
  };
} Figure;

// figure-in.c - ввод обобщенной фигуры

  Figure* FigureIn(FILE* ifst) {
    Figure *pf;
    int k;
    fscanf(ifst, "%d", &(k));
    switch(k) {
      case 1:
        pf = malloc(sizeof(Figure));
        pf->k = RECTANGLE;
        RectangleIn(&(pf->r), ifst);
        return pf;
      case 2:
        pf = malloc(sizeof(Figure));
        pf->k = TRIANGLE;
        TriangleIn(&(pf->t), ifst);
        return pf;
      case 3: // пришлось добавить ввод круга
        pf = malloc(sizeof(Figure));
        pf->k = CIRCLE;
        CircleIn(&(pf->c), ifst);
        return pf;
      default:
        return 0;
    }
  }

  // figure-out.c - вывод обобщенной фигуры
  void FigureOut(Figure *s, FILE* ofst) {
    switch(s->k) {
      case RECTANGLE:
        RectangleOut(&(s->r), ofst);
        break;
      case TRIANGLE:
        TriangleOut(&(s->t), ofst);
        break;
      case CIRCLE: // пришлось добавить вывод круга
        CircleOut(&(s->c), ofst);
        break;
      default:
        fprintf(ofst, "Incorrect figure!\n");
    }
  }

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

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

Шаг 2. Добавление альтернативы при процедурном подходе

Круг и объектно-ориентированный подход

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


  // circle.h - круг
  class Circle: public Figure {
    int r; // радиус
    public:
    // переопределяем интерфейс класса
    void InData(std::ifstream &ifst);  // ввод данных из потока
    void Out(std::ofstream &ofst);     // вывод данных в стандартный поток
    Circle(): r{0} {} // создание без инициализации.
  };

  // circle-in.cpp - ввод круга
  void Circle::InData(std::ifstream &ifst) {
    ifst >> r;
  }

  // circle-out.cpp - вывод круга
  void Circle::Out(std::ofstream &ofst) {
    ofst << "It is Circle: r = " << r << "\n";
  }

Также не вызывает проблем формирование фабрики кругов. Она идентична ранее сформированным фабрикам прямоугольников и треугольников.


  // circle-factory.h - класс фабрики, порождающей круги
  class CircleFactory: public FigureFactory {
    int key; // Ключ фабрики, совпадающий с ключом в файле
    public:
    // иденитификация, порождение и ввод фигуры из потока
    virtual Figure* Test(int k);
    // Конструктор, регистрирующий ключ = 1 для кругов
    CircleFactory();
  };

  // circle-factory.cpp
  // Конструктор, регистрирующий ключ = 3 для кругов
  CircleFactory::CircleFactory(): key{3} {
    factory[counter++] = this;
  }
  // Реализация метода проверки и порождения круга
  Figure* CircleFactory::Test(int k)  {
    if(k == key) {
      return new Circle;
    }
    return nullptr;
  }
  // Регистратор фабрики кругов
  namespace {
    CircleFactory cf;
  }

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

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

Шаг 2. Добавление альтернативы при ОО подходе

Круг и процедурно-параметрический подход

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


  // figure-circle.h - фигура-круг
  Figure + < circ: Circle; >;

  // figure-circle-in - ввод прямоугольника как фигуры
  void FigureIn<Figure.circ *f>(FILE* ifst) {
    CircleIn(&(f->@), ifst);
  }

  // figure-circle-out - вывод круга как фигуры
  void FigureOut<Figure.circ *f>(FILE* ofst) {
    Circle* c = &(f->@);
    CircleOut(c, ofst);
  }

Аналогичным образом расширяются перечислимый тип Input и добавляемая для идентификация круга функция (признак в файле равен 3), что обеспечивает безболезненное расширение ввода фигур, включая круг, из файла.


  // figure-circle.h - признак для ввода круга из файла
  Input + <circ: void;>;

  // figure-circle-out - cоздание abuehs-круга при вводе из файла
  Figure* FigureCreateUseInputTag<Input.circ *pFig>(int k) {
    if(k == 3) {
      return create_spec(Figure.circ);
    } else {
      return NULL;
    }
  }

Весь остальной код повторно используется из программы, разработанной на первом шаге.

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

Шаг 2. Добавление альтернативы при ПП подходе

Выводы

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


Содержание