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

Top.Mail.Ru

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

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


Содержание


Фабричный метод (Factory Method)

Фабричный метод определяет интерфейс для создания объекта, но оставляет подклассам ­решение о том, экземпляры какого класса должны создаваться. Он позволяет классу делегировать создание экземпляров подклассам.

Структура паттерна Фабричный метод

Структура паттерна Фабричный метод

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

Объектно-ориентированная реализация Фабричного метода

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

  class Animal {
  public:
    virtual ~Animal() {}
  // ...
  };

Пусть изначально конкретные разновидности животных будут представлены уткой и собакой:

  class Duck : public Animal {
    public:
    Duck() {std::cout << "Duck created!\n";}
  };
  class Dog : public Animal {
    public:
    Dog() {std::cout << "Dog created!\n";}
  };

Обобщенный создатель, реализованный в соответствии со структурой паттерна будет представлен следующим кодом:

  class Creator {
    public:
    virtual ~Creator(){};
    virtual Animal* FactoryMethod() const = 0;
    Animal* NewAnimal() const {
      return this->FactoryMethod();
    }
  };

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

  class DuckCreator : public Creator {
    public:
    Animal* FactoryMethod() const override {
      return new Duck();
    }
  };

  class DogCreator : public Creator {
    public:
    Animal* FactoryMethod() const override {
      return new Dog();
    }
  };

Взгляд со стороны раздельных данных и функций

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

Модель паттерна Фабричный метод с позиций процедурного подхода

Модель паттерна Фабричный метод с позиций процедурного подхода

Мы видим, что обобщенный тип Animal, определяющий различных животных, содержит некоторый признак, идентифицирующий связанные с ним альтернативы (Duck или Dog). Конкретная специализация обобщения, порождается за счет вызова фабричного метода (FactoryMethod), который, в зависимости от признака, определяющего вид создаваемого животного (DuckCreator или DogCreator), может вызывать одну из двух представленных функций. Одна из них формирует утку как специализацию животного, а другая, аналогичным образом, формирует специализацию для собаки.

Процедурная реализация Фабричного метода

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

  // Конкретные животные/
  typedef struct Duck {} Duck;
  typedef struct Dog {} Dog;

  // Признаки конкретных животных
  typedef enum AnimalTag {duckTag, dogTag,} AnimalTag;

  // Животное. Непосредственно включает конкретных животных
  typedef struct Animal {
    AnimalTag tag;
    union {
      Duck duck;
      Dog dog;
    };
  } Animal;

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

  typedef enum Creator {
    DuckCreator,
    DogCreator,
  } Creator;

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

Фабричный метод и используемые им дополнительные функции обеспечивают формирование конкретных специализаций.

// Создание конкретных животных
Animal* DuckFactoryMethod() {
  Animal* p = malloc(sizeof(Animal));
  p->tag = duckTag;
  printf("Duck created!\n");
  return p;
}
Animal* DogFactoryMethod() {
  Animal* p = malloc(sizeof(Animal));
  p->tag = dogTag;
  printf("Dog created!\n");
  return p;
}

// Создание животного в зависимости от Создателя
Animal* FactoryMethod(Creator c) {
  switch (c) {
    case DuckCreator: return DuckFactoryMethod();
    case DogCreator: return DogFactoryMethod();
    default: return NULL;
  }
}

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

  Animal* NewAnimal(Creator c) {
    // Вызываем фабричный метод, чтобы получить обобщенного животного.
    Animal* animal = FactoryMethod(c);
    return animal;
  }

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

Процедурно-параметрическая имитация Фабричного метода

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

  // Обобщение для животных.
  typedef struct Animal {}<> Animal;

  // Конкретные животные. Могут в будущем иметь специализации...
  typedef struct Duck {}<> Duck;
  Animal + <Duck;>;
  typedef struct Dog {}<> Dog;
  Animal + <Dog;>;

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

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

  typedef struct Creator {}<> Creator;

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

  Animal* FactoryMethod<Creator* c>() {return NULL;} 

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

  Animal* NewAnimal(Creator* c) {
    // Вызываем фабричный метод, чтобы создать животное.
    Animal* animal = FactoryMethod<c>();
    return animal;
  }

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

  typedef struct DuckCreator {} DuckCreator;
  Creator + <DuckCreator;>;
  Animal* FactoryMethod<Creator.DuckCreator* p>() {
    printf("Duck created!\n");
    return create_spec(Animal.Duck);
  }
  typedef struct DogCreator {} DogCreator;
  Creator + <DogCreator;>;
  Animal* FactoryMethod<Creator.DogCreator* p>() {
    printf("Dog created\n");
    return create_spec(Animal.Dog);
  }

Что показывает ПП имитация Фабричного метода

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

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

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

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


Содержание