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

Top.Mail.Ru

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

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


Содержание


Шаг первый. Начальная программа

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

Первоначальная процедурная программа

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


  // rectangl.h - прямоугольник
  typedef struct Rectangle {
    int x, y; // ширина, высота
  } Rectangle;

  // rectangle-in.c - ввод прямоугольника
  void RectangleIn(Rectangle *r, FILE* ifst) {
    fscanf(ifst, "%d", &(r->x));
    fscanf(ifst, "%d", &(r->y));
  }

  // rectangle-out.c - вывод прямоугольника
  void RectangleOut(Rectangle *r, FILE* ofst) {
    fprintf(ofst, "It is Rectangle: x = %d, y = %d\n", r->x, r->y);
  }

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

  // triangle-in.c - ввод треугольника
  void TriangleIn(Triangle *t, FILE* ifst) {
    fscanf(ifst, "%d", &(t->a));
    fscanf(ifst, "%d", &(t->b));
    fscanf(ifst, "%d", &(t->c));
  }

  // triangle-out.c - вывод треугольника
  void TriangleOut(Triangle *t, FILE *ofst) {
    fprintf(ofst, "It is Triangle: a = %d, b = %d, c = %d\n", t->a, t->b, t->c);
  }

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


  // figure.h - обобщенная фигура
  typedef enum key {RECTANGLE, TRIANGLE} key; // признаки фигур

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

  // figure-in.c
  // Прототипы функций ввода основ специализаций
  void RectangleIn(Rectangle *r, FILE* ifst);
  void TriangleIn(Triangle *t, FILE* ifst);
  // Ввод обобщенной фигуры
  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;
      default:
      return 0;
    }
  }

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

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


  // container.h
  // Ссылка на используемую структуру (достаточно ее)
  struct Figure;
  enum {max_len = 100}; // максимальная длина
  // Простейший контейнер на основе одномерного массива
  typedef struct Container {
    int len; // текущая длина
    struct Figure *cont[max_len]; // массив для хранения фигур
  } Container;

  // container-consr
  // Инициализация контейнера
  void ContainerInit(Container *c) { c->len = 0; }
  // Очистка контейнера от элементов (освобождение памяти)
  void ContainerClear(Container *c) {
    for(int i = 0; i < c->len; i++) {
      free(c->cont[i]);
    }
    ContainerInit(c);
  }

  // container-in
  struct Figure *FigureIn(FILE* ifdt); // прототип ввода фигуры
  // Ввод содержимого контейнера
  void ContainerIn(Container* c, FILE* ifst) {
    while(!feof(ifst))  {
      if((c->cont[c->len] = FigureIn(ifst)) != 0) {
        c->len++;
      }
    }
  }

  // container-out
  void FigureOut(struct Figure* s, FILE* ofst); // Прототип вывода фигуры
  // Вывод содержимого контейнера
  void ContainerOut(Container *c, FILE* ofst) {
    fprintf(ofst, "Container contains %d elements.\n", c->len);
    for(int i = 0; i < c->len; i++) {
      fprintf(ofst, "%d: " , i);
      FigureOut(c->cont[i], ofst);
    }
  }

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


  // main.c
  int main(int argc, char* argv[]) {
    if(argc !=3) {
      printf("incorrect command line!\nWaited: command infile outfile\n");
      return 1;
    }
    FILE* ifst = fopen(argv[1], "r");
    FILE* ofst = fopen(argv[2], "w");

    Container c;
    ContainerInit(&c);
    ContainerIn(&c, ifst);
    fclose(ifst);

    fprintf(ofst, "Filled container.\n");
    ContainerOut(&c, ofst);

    ContainerClear(&c);
    fclose(ofst);
    return 0;
  }

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

Шаг 1. Зависимость процедурных альтернатив

Первоначальная объектно-ориентированная программа

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


  // figure.h - обобщенная фигура
  class Figure {
    public:
    // Интерфейс
    virtual void In(std::ifstream &ifst) = 0;  // ввод
    virtual void Out(std::ofstream &ofst) = 0; // вывод
  };

  // rectangle.h - прямоугольник
  class Rectangle: public Figure {
    int x, y; // ширина, высота
    public:
    // переопределяем интерфейс класса
    virtual void In(std::ifstream &ifst);  // ввод
    virtual void Out(std::ofstream &ofst); // вывод
    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); // вывод
    Triangle(): a{0}, b{0}, c{0} {} // создание без инициализации.
  };

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


  // rectangle-in.cpp - ввод прямоугольника
  void Rectangle::In(std::ifstream &ifst) {
    ifst >> x >> y;
  }

  // rectangle-out.cpp - вывод прямоугольника
  void Rectangle::Out(std::ofstream &ofst) {
    ofst << "It is Rectangle: x = " << x << ", y = " << y << "\n";
  }

  // triangle-in.cpp - ввод треугольника
  void Triangle::In(std::ifstream &ifst) {
    ifst >> a >> b >> c;
  }

  // triangle-out.cpp - вывод треугольника
  void Triangle::Out(std::ofstream &ofst) {
    ofst << "It is Triangle: a = "
    << a << ", b = " << b
    << ", c = " << c << "\n";
  }

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


  // figure-factory.h - класс, общий для всех фабрик
  class Figure;  // ссылка на класс обобщенной фигуры
  class FigureFactory {
    protected:
    static int counter;   // счетчик зарегистрированных фабрик
    static FigureFactory* factory[]; // массив указателей на фабрики
    public:
    // иденитификация, порождение и ввод фигуры из потока
    static  Figure* Create(std::ifstream &ifst);    // обход конкретных фабрик
    virtual Figure* Test(int k) = 0;  // проверка ключа конкретной фабрикой
  };

  // figure-factory.cpp - реализация методов обобщенной фабрики
  int FigureFactory::counter = 0;
  FigureFactory* FigureFactory::factory[10];

  // Реализация метода обхода зарегистрированных фабрик
  Figure* FigureFactory::Create(std::ifstream &ifst) {
    int k;
    ifst >> k;
    for(int i = 0; i < FigureFactory::counter; ++i) {
      Figure* f = factory[i]->Test(k);
      if(f != nullptr) {
        f->In(ifst);
        // f->Out();
        return f;
      }
    }
    return nullptr;
  }

Абстрактная фабрика фигур осуществляет обход зарегистрированных фабрик конкретных фигур, используя статический метод Create. Он читает из файла признак фигуры и передает его в метод Test, переопределяемый в каждой конкретной фабрике. Конкретные фабрики регистрируются в factory (для простенького примера используется простейший одномерный массив). Число зарегистрированных фабрик конкретных фигур фиксируется в переменной counter.

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


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

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

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

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

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

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

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


  // 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 Clear();  // очистка контейнера от фигур
    Container();    // инициализация контейнера
    ~Container() {Clear();} // утилизация контейнера перед уничтожением
  };

  // container-constr.cpp - инициализатор и очистка
  Container::Container(): len{0} { }
  // очистка контейнера от элементов
  void Container::Clear() {
    for(int i = 0; i < len; i++) {
      delete cont[i];
    }
    len = 0;
  }

  // container-in.cpp - ввод содержимого контейнера
  void Container::In(std::ifstream &ifst) {
    while(!ifst.eof()) {
      if((cont[len] = FigureFactory::Create(ifst)) != nullptr) {
        len++;
      }
    }
  }

  // container-out.cpp - вывод содержимого контейнера
  void Container::Out(std::ofstream &ofst) {
    ofst << "Container contains " << len << " elements.\n";
    for(int i = 0; i < len; i++) {
      ofst << i << ": ";
      cont[i]->Out(ofst);
    }
  }

Организация клиента практически идентична процедурному решению.


  int main(int argc, char* argv[]) {
    if(argc !=3) {
      std::cout << "incorrect command line! Wated: command infile outfile\n";
      return 1;
    }
    std::ifstream ifst(argv[1]);
    std::ofstream ofst(argv[2]);

    Container c;
    c.In(ifst);

    ofst << "Filled container.\n";
    c.Out(ofst);

    c.Clear();
    ofst << "Empty container.\n";
    c.Out(ofst);
    return 0;
  }

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

Шаг 1. Зависимость ОО альтернатив

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

Первоначальная процедурно-параметрическая программа

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


  // figure.h - структура, обобщающая фигуры
  typedef struct Figure {} <> Figure;

  // figure-in.c - обобщающая функция для ввода фигуры
  void FigureIn<Figure *f>(FILE* file) {} //= 0;

  // figure-out.c - обобщающая функция для вывода фигуры
  void FigureOut<Figure *f>(FILE* file) {} //= 0;

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

  // figure-rectangle.h - фигура-прямоугольник
  Figure + < rect: Rectangle; >;

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

  // figure-rectangle-out.c - вывод прямоугольника как фигуры
  void FigureOut<Figure.rect *f>(FILE* ofst) {
    RectangleOut(&(f->@), ofst);
  }

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

  // figure-triangle.h - фигура-треугольник
  Figure + < trian: Triangle; >;

  // figure-triangle-in.c - ввод треугольника как фигуры
  void FigureIn<Figure.trian *f>(FILE* ifst) {
    TriangleIn(&(f->@), ifst);
  }

  // figure-triangle-out.c - вывод треугольника как фигуры
  void FigureOut<Figure.trian *f>(FILE* ofst) {
    TriangleOut(&(f->@), ofst);
  }

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

Возможности динамического полиморфизма в данном случае, как и при ОО подходе позволяют организовать ввод данных без явной проверки признаков и изменения ранее написанного кода. Однако при ПП подходе, с сохранением общей идеи, используются другие механизмы и специальные функции, реализованные с учетом особенностей 4П. Функция FigureCreateAndIn, вызывая встроенную в язык функцию get_spec_size, получает количество зарегистрированных специализаций заданного обобщения (в данном случае обобщения Input, число альтернатив в котором совпадает с числом фигур, но можно непосредственно использовать и обобщение Figure) . Учитывая, что признаки специализаций для любого обобщения нумеруются с 1 до числа специализаций (нулевое значение резервируется за самим обобщением), можно, получить внутренний (фиктивный) указатель на специализацию обобщения заданного типа, имеющую соответствующий признак (используется встроенная функция get_spec_ptr). В цикле обхода признаков специализаций вызывается функция FigureCreateUseInputTag, обеспечивающая полиморфное создание нужной фигуры по полученному указателю на специализацию и ключу типа фигуры, прочитанному из файла. После этого осуществляется полиморфный ввод данных для уже созданной фигуры.


  // figures-input.c - ввод параметров одной из фигур из файла
  Figure* FigureCreateAndIn(FILE* ifst) {
    int inSpecSize = get_spec_size(Input);
    Figure *sp;
    int k = 0;
    fscanf(ifst, "%d", &(k));
    for(int i = 1; i < inSpecSize; i++) {
      Input* pIn = get_spec_ptr(Input, i);
      sp = FigureCreateUseInputTag<pIn>(k);
      if(sp != NULL) break;
    }
    if(sp == NULL) {
      printf("Incorrect pointer to input figure\n");
      exit(13);
    }
    FigureIn<sp>(ifst);
    return sp;
  }

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


  // figure.h - расширяемый перечислимый тип для обобщения ввода данных
  typedef struct Input {} <> Input;

  // Обобщающая функция, которая по указателю на специализацию
  // и значению из файла создает специализированную фигуру
  Figure* FigureCreateUseInputTag<Input *pFig>(int k) {
    printf("Unnown figure type for k = %d\n", k);
    exit(13);
  }

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

  // figure-rectangle.h - расширение перечисления для прямоугольников
  Input + <rect: void;>;

  // figure-rectangle-in.c создание фигуры-прямоугольника клонированием
  Figure* FigureCreateUseInputTag<Input.rect *pFig>(int k) {
    if(k == 1) {
      return create_spec(Figure.rect);
    } else {
      return NULL;
    }
  }

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

  // figure-triangle.h - расширение перечисления для треугольников
  Input + <trian: void;>;

  // figure-triangle-in.c - создание фигуры-треугольника клонированием
  Figure* FigureCreateUseInputTag<Input.trian *pFig>(int k) {
    if(k == 2) {
      return create_spec(Figure.trian);
    } else {
      return NULL;
    }
  }

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


  // container-in.c -  ввод фигур в контейнер
  void ContainerIn(Container* c, FILE* ifst) {
    while(!feof(ifst))  {
      if((c->cont[c->len] = FigureCreateAndIn(ifst)) != 0) {
        c->len++;
      }
    }
  }

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


  int main(int argc, char* argv[]) {
    if(argc !=3) {
      printf("incorrect command line!\nWaited: command infile outfile\n");
      return 1;
    }
    FILE* ifst = fopen(argv[1], "r");
    FILE* ofst = fopen(argv[2], "w");

    Container c;
    ContainerInit(&c);
    ContainerIn(&c, ifst);
    fclose(ifst);

    fprintf(stdout, "Filled container.\n");
    ContainerOut(&c, stdout);

    ContainerClear(&c);
    fprintf(stdout, "Empty container.\n");
    ContainerOut(&c, stdout);
    fclose(ofst);

    return 0;
  }

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

Шаг 1. Зависимость процедурно-параметрических альтернатив

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

Выводы

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


Содержание