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

Top.Mail.Ru

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

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


Содержание


Строитель (Builder)

Строитель отделяет конструирование сложного объекта от его представления. В результате одного и того же процесса конструирования могут получаться разные представления. Он применяется, когда:

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

Структура паттерна "Строитель"

Объектно-ориентированная реализация Строителя

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

Будем хранить протокол покупок в массиве, имитирующем некоторую "Амбарную книгу":


  // Состав животных на ферме или в коллекции
  class Animals{
    public:
    std::vector<std::string> parts_;
    void ListParts()const{
      std::cout << "Animals:\n";
      for (size_t i=0;i<parts_.size();i++){
        if(parts_[i]== parts_.back()){
          std::cout << "    " << parts_[i];
        }else{
          std::cout << "    " << parts_[i] << ",\n";
        }
      }
      std::cout << "\n\n";
    }
  };

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


  // Интерфейс Строителя объявляет методы для приобретения животных.
  class Builder{
    public:
    virtual ~Builder(){}
    virtual void BuyDuck() const =0;
    virtual void BuyDog() const =0;
  };

  // Классы Конкретного Строителя следуют интерфейсу предоставляют конкретные
  // шаги построения. Программа может иметь нескольковариантов Строителей,
  // реализованных по-разному.

  class DomesticFarmBuilder : public Builder{
  private:
      Animals* farm;
      // Новый экземпляр строителя должен содержать пустой объект продукта,
      // который используется в дальнейшей сборке.
  public:
      DomesticFarmBuilder(){
          this->Reset();
      }
      ~DomesticFarmBuilder(){
          delete farm;
      }
      void Reset(){
          this->farm = new Animals();
      }
      // Все этапы производства работают с одним и тем же экземпляром продукта.
      void BuyDuck()const override{
          // Здесь можно еще и создать купленную утку
          this->farm->parts_.push_back("We have bought the Domestic Duck");
      }
      void BuyDog()const override{
          // Здесь можно еще и создать купленную собаку
          this->farm->parts_.push_back("We have bought the Domestic Dog");
      }
      // Конкретные Строители должны предоставить свои собственные методы
      // получения результатов. Это связано с тем, что различные типы строителей
      // могут создавать совершенно разные продукты с разными интерфейсами.
      Animals* GetProduct() {
          Animals* result = this->farm;
          this->Reset();
          return result;
      }
  };

  class ToyFarmBuilder : public Builder{
    private:
    Animals* farm;
    // Новый экземпляр строителя должен содержать пустой объект продукта,
    // который используется в дальнейшей сборке.
    public:
    ToyFarmBuilder(){
      this->Reset();
    }
    ~ToyFarmBuilder(){
      delete farm;
    }
    void Reset(){
      this->farm = new Animals();
    }
    // Все этапы производства работают с одним и тем же экземпляром продукта.
    void BuyDuck()const override {
      // Здесь можно еще и создать игрушечную купленную утку
      this->farm->parts_.push_back("We have bought the Toy Duck");
    }
    void BuyDog()const override{
      // Здесь можно еще и создать купленную игрушечную собаку
      this->farm->parts_.push_back("We have bought the Toy Dog");
    }
    // Конкретные Строители должны предоставить свои собственные методы
    // получения результатов. Это связано с тем, что различные типы строителей
    // могут создавать совершенно разные продукты с разными интерфейсами.
    Animals* GetProduct() {
      Animals* result = this->farm;
      this->Reset();
      return result;
    }
  };

Следует отметить, что в общем случае строители могут быть совершенно различными.

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


  class Director{
    private:
    Builder* builder;
    // Директор работает с любым экземпляром строителя, который передаётся ему
    // клиентским кодом. Таким образом, клиентский код может изменить конечный
    // тип вновь собираемого продукта.
    public:
    void set_builder(Builder* builder){
      this->builder=builder;
    }
    // Директор может строить несколько вариаций продукта, используя одинаковые
    // шаги построения.
    void BuildMinimalViableProduct(){
      this->builder->BuyDuck();
    }
    void BuildFullFeaturedProduct(){
      this->builder->BuyDuck();
      this->builder->BuyDog();
    }
  };

Строитель с позиций процедурного подхода

В Строители используется только одна альтернатива, определяющая варианты субпродуктов, из которых формируется составная конструкция. Количество различных субпродуктов, определяет интерфей с Строителя, вырождающийся в набор полиморфных функций. В продолжение работы с животными выделим пару таких функций, обеспечивающих добавление уток и собак (рисунок~\ref{ppp-builder}). В данной ситуации обработчики получают специализированный строитель также в виде перечислимого типа. Обычные функции--конструкторы используют конкретных Cтроителей для построения из отдельных частей нужных конфигураций. Они также могут получать дополнительные параметры, используемые для взаимодействия с окружающей средой.

Графическое представление ПП аналога Строителя

Графическое представление ПП аналога Строителя

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

Учитывая, что это, все-таки, C а не C++, сделаем замену библиотечного вектора на более простую структуру для хранения строк символов:


  typedef struct Animals {
    int animal_size;
    char animalsList[10][128];
  } Animals;

  void ListParts(Animals* a) {
    printf("Animal list:\n");
    for (int i=0; i< a->animal_size; i++){
      if(i == a->animal_size - 1) {
        printf("    %s", a->animalsList[i]);
      } else {
        printf("    %s,\n", a->animalsList[i]);
      }
    }
    printf("\n\n");
  }

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


  typedef struct Builder{}<> Builder;
  void BuyDuck<Builder* b>() = 0;
  void BuyDog<Builder* b>()  = 0;

  typedef struct DomesticFarmBuilder {Animals* animals;} DomesticFarmBuilder;
  Builder + <DomesticFarmBuilder;>;

  void BuyDuck<Builder.DomesticFarmBuilder* b>() {
    Animals* a = b->@animals;
    strcpy(a->animalsList[a->animal_size++], "We have bought the Domestic Duck");
  }
  void BuyDog<Builder.DomesticFarmBuilder* b>() {
    Animals* a = b->@animals;
    strcpy(a->animalsList[a->animal_size++], "We have bought the Domestic Dog");
  }

  // Новый экземпляр строителя должен содержать пустой объект продукта,
  // который используется в дальнейшей сборке.
  void ResetDomestic(DomesticFarmBuilder* b){
    b->animals= malloc(sizeof(Animals));
    b->animals->animal_size = 0;
  }

  void DomesticFarmBuilderInit(DomesticFarmBuilder* b){
    ResetDomestic(b);
  }
  void DomesticFarmBuilderDelete(DomesticFarmBuilder* b){
    free(b->animals);
  }

  Animals* GetDomesticResult(DomesticFarmBuilder* b) {
    Animals* result= b->animals;
    ResetDomestic(b);
    return result;
  }

  typedef struct ToyFarmBuilder {Animals* animals;} ToyFarmBuilder;
  Builder + <ToyFarmBuilder;>;

  void BuyDuck<Builder.ToyFarmBuilder* b>() {
    Animals* a = b->@animals;
    strcpy(a->animalsList[a->animal_size++], "We have bought the Toy Duck");
  }
  void BuyDog<Builder.ToyFarmBuilder* b>() {
    Animals* a = b->@animals;
    strcpy(a->animalsList[a->animal_size++], "We have bought the Toy Dog");
  }

  void ResetToy(ToyFarmBuilder* b){
    b->animals= malloc(sizeof(Animals));
    b->animals->animal_size = 0;
  }

  void ToyFarmBuilderInit(ToyFarmBuilder* b){
    ResetToy(b);
  }
  void ToyFarmBuilderDelete(ToyFarmBuilder* b){
    free(b->animals);
  }

  Animals* GetToyResult(ToyFarmBuilder* b) {
    Animals* result= b->animals;
    ResetToy(b);
    return result;
  }

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


  // Директор отвечает за выполнение шагов построения в определённой последовательности.
  typedef struct Director{
    Builder* builder;
  } Director;

  // Директор работает с любым экземпляром строителя, который передаётся ему
  // клиентским кодом. Таким образом, клиентский код может изменить конечный
  // тип вновь собираемого продукта.
  void SetBuilder(Director* d, Builder* builder){
    d->builder = builder;
  }

  // Директор может строить несколько вариаций продукта, используя одинаковые
  // шаги построения.
  void BuildMinimalViableProduct(Director* d){
    BuyDuck<d->builder>();
  }

  void BuildFullFeaturedProduct(Director* d){
    BuyDuck<d->builder>();
    BuyDog<d->builder>();
  }

Строитель и ППП

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


Содержание