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

Top.Mail.Ru

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

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


Содержание


Декоратор (Decorator)

ОО образец "Декоратор" динамически добавляет объекту новые обязанности. Является гибкой альтернативой порождению подклассов с целью расширения функциональности. Используется:

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

Графическое представление ОО структуры Декоратора приведено на рисунке.

Структура паттерна Декоратор

Структура паттерна Декоратор

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

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


  // Обобщенное животное, определяемое в базовом классе.
  // Также используется для обобщение декораторов.
  class Animal {
  public:
    virtual ~Animal() {}
    virtual void GetInfo() const = 0;
  };

  // Утка
  class Duck : public Animal {
  public:
    void GetInfo() const override {
      std::cout << "I am the Duck";
    }
  };
  // Собака
  class Dog : public Animal {
  public:
    void GetInfo() const override {
      std::cout << "I am the Dog";
    }
  };

  // Абстрактный декоратор для расширения cdjqcnd животных
  class Decorator : public Animal {
  protected:
    Animal* animal;
  public:
    Decorator(Animal* a) : animal{a} {}
    void GetInfo() const override {animal->GetInfo();}
  };
  // Конкретные Декораторы вызывают обёрнутый объект
  // и изменяют его результат некоторым образом.
  class Age : public Decorator {
    int age;
  public:
    Age(Animal* a, int _age) : age{_age}, Decorator(a) {}
    void GetInfo() const override {
      Decorator::GetInfo();
      std::cout << ", I am " << age << " yeas old.";
    }
  };
  class Name : public Decorator {
    std::string name;
  public:
    Name(Animal* a, std::string n) : name{n}, Decorator(a) {}
    void GetInfo() const override {
      std::cout << "My name is " << name << ". ";
      Decorator::GetInfo();
    }
  };

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


  // Клиент запускает сформированных животных
  void ClientCode(Animal* a) {
    std::cout << "\n";
    a->GetInfo();
    std::cout << "\n";
  }

  int main() {
    std::cout << "Different Animals:\n";
    Duck duck;
    Dog  dog;

    ClientCode(&duck);
    ClientCode(&dog);

    Age duckAge{&duck, 2};
    ClientCode(&duckAge);

    Age dogAge{&dog, 5};
    Name dogName{&dogAge, "Rex"};
    ClientCode(&dogName);

    Name otherDogName{&dog, "Mu-mu"};
    Age otherDogAge{&otherDogName, 3};
    ClientCode(&otherDogAge);

    return 0;
  }

Процедурная схема Декоратора

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

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

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

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

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


  // Обобщенное животное
  typedef struct Animal {}<> Animal;
  void GetInfo<Animal* a>() = 0;

  // Утка
  typedef struct Duck {} Duck;
  Animal + <Duck;>;

  void GetInfo<Animal.Duck* a>() {
    printf("I am the Duck");
  };

  // Собака
  typedef struct Dog {} Dog;
  Animal + <Dog;>;

  void GetInfo<Animal.Dog* a>() {
    printf("I am the Dog");
  };

После этого можно приступать к имитации декораторов и функций, осуществляющих их обработку. В соответствии приведенной на рисунке~\ref{ppp-decorator} процедурной схемой сформированы обобщенный декоратор и его специализации. Последние также проще создать из ранее сформированных основ, в которых задаются соответствующие возраст и имя.


  // Абстрактный декоратор для расширения животных
  typedef struct Decorator {Animal* animal;}<> Decorator;
  // Связывание декоратора с обобщенным животным
  void InitDecorator(Decorator* d, Animal* a) {
    d->animal = a;
  }
  // Информация, порождаемая декораторами
  void GetDecoratorInfo<Decorator* d>() = 0;

  Animal + <Decorator;>;

  // Декоратор делегирует всю работу обёрнутому животному.
  void GetInfo<Animal.Decorator* a>() {
    GetDecoratorInfo<&(a->@)>();
  }

  // Конкретные Декораторы вызывают обёрнутый объект
  // и изменяют его результат некоторым образом.
  typedef struct Age {int age;} Age;
  void InitAge(Age* a, int v) {
    a->age = v;
  }

  Decorator + <Age;>;
  void InitAgeDecorator(struct Decorator.Age* da, Animal* a, int v) {
    InitDecorator((Decorator*)da, a);
    InitAge(&(da->@), v);
  }
  void GetDecoratorInfo<Decorator.Age* d>() {
    // Перенаправление дальше
    GetInfo<d->animal>();
    printf(", I am %d yeas old.", d->@age);
  }
  typedef struct Name {char* name;} Name;
  void InitName(Name* a, char* n) {
    a->name = n;
  }

  Decorator + <Name;>;
  void InitNameDecorator(struct Decorator.Name* dn, Animal* a, char* n) {
    InitDecorator((Decorator*)dn, a);
    InitName(&(dn->@), n);
  }
  void GetDecoratorInfo<Decorator.Name* d>() {
    printf("My name is %s. ", d->@name);
    GetInfo<d->animal>();
  }

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

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


  // Клиент запускает сформированных животных
  void ClientCode(Animal* a) {
    printf("\n");
    GetInfo<a>();
    printf("\n");
  }

  int main() {
    printf("Different Animals:\n");
    struct Animal.Duck duck;
    struct Animal.Dog  dog;

    ClientCode((Animal*)&duck);
    ClientCode((Animal*)&dog);

    struct Animal.Decorator.Age duckAge;
    InitAgeDecorator(&(duckAge.@), (Animal*)&duck, 2);
    ClientCode((Animal*)&duckAge);

    struct Animal.Decorator.Name dogName;
    struct Animal.Decorator.Age  dogAge;
    InitNameDecorator(&(dogName.@), (Animal*)&dog, "Rex");
    InitAgeDecorator(&(dogAge.@), (Animal*)&dogName, 10);
    ClientCode((Animal*)&dogAge);

    struct Animal.Decorator.Name otherDogName;
    struct Animal.Decorator.Age otherDogAge;
    InitNameDecorator(&(otherDogName.@), (Animal*)&otherDogAge, "Mu-mu");
    InitAgeDecorator(&(otherDogAge.@), (Animal*)&dog, 5);
    ClientCode((Animal*)&otherDogName);

    return 0;
  }

Нужен ли обобщенный процедурно-параметрический декоратор?

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

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

Графическое представление ПП версии только с конкретными декораторами

Графическое представление ПП версии только с конкретными декораторами

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


  //--------------------------------------------------------
  // Конкретные Декораторы вызывают обёрнутый объект
  // и изменяют его результат некоторым образом.
  //--------------------------------------------------------
  typedef struct AgeDecorator {Animal* animal; int age;} AgeDecorator;
  void InitAgeDecorator(struct AgeDecorator* da, Animal* a, int age) {
    da->animal = a;
    da->age = age;
  }

  Animal + <AgeDecorator;>;
  void GetInfo<Animal.AgeDecorator* a>() {
    // Перенаправление дальше
    GetInfo<a->@animal>();
    printf(", I am %d yeas old.", a->@age);
  }

  typedef struct NameDecorator {Animal* animal; char* name;} NameDecorator;
  void InitNameDecorator(struct NameDecorator* dn, Animal* a, char* n) {
    dn->animal = a;
    dn->name = n;
  }

  Animal + <NameDecorator;>;
  void GetInfo<Animal.NameDecorator* a>() {
    printf("My name is %s. ", a->@name);
    // Перенаправляем дальше
    GetInfo<a->@animal>();
  }

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


  int main() {
    printf("Different Animals:\n");
    struct Animal.Duck duck;
    struct Animal.Dog  dog;

    ClientCode((Animal*)&duck);
    ClientCode((Animal*)&dog);

    struct Animal.AgeDecorator duckAge;
    InitAgeDecorator(&(duckAge.@), (Animal*)&duck, 2);
    ClientCode((Animal*)&duckAge);

    struct Animal.NameDecorator dogName;
    struct Animal.AgeDecorator  dogAge;
    InitNameDecorator(&(dogName.@), (Animal*)&dog, "Rex");
    InitAgeDecorator(&(dogAge.@), (Animal*)&dogName, 10);
    ClientCode((Animal*)&dogAge);

    struct Animal.NameDecorator otherDogName;
    struct Animal.AgeDecorator  otherDogAge;
    InitNameDecorator(&(otherDogName.@), (Animal*)&otherDogAge, "Mu-mu");
    InitAgeDecorator(&(otherDogAge.@), (Animal*)&dog, 5);
    ClientCode((Animal*)&otherDogName);

    return 0;
  }

Использование в декораторах указателей на их же обобщения

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

Графическое представление ПП версии с указателем на обобщение, используемого в качестве специализации

Графическое представление ПП версии с указателем на обобщение, используемого в качестве специализации

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


  //------------------------------------------------------------------------------
  // Использование отдельных декораторов, непосредственно подключаемых к животным
  //------------------------------------------------------------------------------

  // Декоратор, добавляющий возраст
  typedef struct AgeDecorator {int age;}<animal: Animal*;> AgeDecorator;
  void InitAgeDecorator(struct AgeDecorator.animal* da, Animal* a, int age) {
    da->age = age;
    da->@ = a;
  }

  Animal + <AgeDecorator;>;
  void GetInfo<Animal.AgeDecorator* a>() {
    // Перенаправление дальше на животного
    struct AgeDecorator.animal *d = (struct AgeDecorator.animal*)&a->@;
    // struct Animal* aa = d->@;
    // GetInfo<aa>();
    GetInfo<(Animal*)d->@>();
    printf(", I am %d yeas old.", a->@age);
  }

  // Декоратор, добавляющий имя
  typedef struct NameDecorator {char* name;}<animal: Animal*;> NameDecorator;
  void InitNameDecorator(struct NameDecorator.animal* da, Animal* a, char* name) {
    da->name = name;
    da->@ = a;
  }

  Animal + <NameDecorator;>;
  void GetInfo<Animal.NameDecorator* a>() {
    printf("My name is %s. ", a->@name);
    // Перенаправление дальше на животного
    struct NameDecorator.animal* d = (struct NameDecorator.animal*)&a->@;
    GetInfo<(Animal*)d->@>();
  }

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


  int main() {
    printf("Different Animals:\n");
    struct Animal.Duck duck;
    struct Animal.Dog  dog;
    ClientCode((Animal*)&duck);
    ClientCode((Animal*)&dog);

    struct Animal.AgeDecorator.animal duckAge;
    InitAgeDecorator(&(duckAge.@), (Animal*)&duck, 2);
    ClientCode((Animal*)&duckAge);

    struct Animal.NameDecorator.animal dogName;
    struct Animal.AgeDecorator.animal  dogAge;
    InitNameDecorator(&(dogName.@), (Animal*)&dog, "Rex");
    InitAgeDecorator(&(dogAge.@), (Animal*)&dogName, 10);
    ClientCode((Animal*)&dogAge);

    struct Animal.AgeDecorator.animal  otherDogAge;
    struct Animal.NameDecorator.animal otherDogName;
    InitAgeDecorator(&(otherDogAge.@), (Animal*)&dog, 10);
    InitNameDecorator(&(otherDogName.@), (Animal*)&otherDogAge, "Mu-mu");
    ClientCode((Animal*)&otherDogName);

    return 0;
  }

Имитация Декоратора через монолитные композиции специализаций

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

Графическое представление ПП версии с непосредственным включением обобщения в качестве специализации

Графическое представление ПП версии с непосредственным включением обобщения в качестве специализации

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


  //------------------------------------------------------------------------------
  // Использование отдельных декораторов,
  // непосредственно подключаемых к животным
  //------------------------------------------------------------------------------

  // Декоратор, добавляющий возраст
  typedef struct AgeDecorator {int age;}<Animal;> AgeDecorator;
  void InitAgeDecorator(AgeDecorator* a, int age) {
    a->age = age;
  }
  void GetAgeDecoratorInfo<AgeDecorator* d>() = 0;
  // AgeDecorator + <Animal;>;
  void GetAgeDecoratorInfo<AgeDecorator.Animal* d>() {
    // Перенаправление дальше на животного
    GetInfo<&(d->@)>();
    printf(", I am %d yeas old.", d->age);
  }

  Animal + <AgeDecorator;>;
  void GetInfo<Animal.AgeDecorator* a>() {
    // Перенаправление дальше на животного
    struct AgeDecorator.Animal* d = (struct AgeDecorator.Animal*)&(a->@);
    GetInfo<(Animal*)&(d->@)>();
    printf(", I am %d yeas old.", a->@age);
  }

  // Декоратор, добавляющий имя
  typedef struct NameDecorator {char* name;}<Animal;> NameDecorator;
  void InitNameDecorator(NameDecorator* a, char* name) {
    a->name = name;
  }

  Animal + <NameDecorator;>;
  void GetInfo<Animal.NameDecorator* a>() {
    printf("My name is %s. ", a->@name);
    // Перенаправление дальше на животного
    struct NameDecorator.Animal* d = (struct NameDecorator.Animal*)&(a->@);
    GetInfo<(Animal*)&(d->@)>();
  }

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


  int main() {
    printf("Different Animals:\n");
    struct Animal.Duck duck;
    struct Animal.Dog  dog;

    ClientCode((Animal*)&duck);
    ClientCode((Animal*)&dog);

    struct Animal.AgeDecorator.Animal.Duck duckAge;
    InitAgeDecorator((AgeDecorator*)&(duckAge.@age), 2);
    ClientCode((Animal*)&duckAge);

    struct Animal.NameDecorator.Animal.AgeDecorator.Animal.Dog dogNameAge;
    InitNameDecorator((NameDecorator*)&(dogNameAge.@), "Rex");
    InitAgeDecorator((AgeDecorator*)&(dogNameAge.@.@.@), 10);
    ClientCode((Animal*)&dogNameAge);

    struct Animal.AgeDecorator.Animal.NameDecorator.Animal.Dog otherDogAgeName;
    InitAgeDecorator((AgeDecorator*)&(otherDogAgeName.@), 5);
    InitNameDecorator((NameDecorator*)&(otherDogAgeName.@.@.@), "Mu-mu");
    ClientCode((Animal*)&otherDogAgeName);

    return 0;
  }

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


Содержание