CPP

Шаблон проектирования прототипов — это порождающий шаблон проектирования, который помогает в прототипировании (недорогом создании/копировании) объекта с использованием отдельных методов или полиморфных классов. Прототипом можно считать модель объекта, на основе которой будет построен реальный объект. В этой статье о порождающих шаблонах проектирования мы разберёмся, для чего нужен шаблон проектирования прототипов в C++, рассмотрим фабрику прототипов и использование шаблона проектирования прототипов для реализации виртуального конструктора копирования.

В статье использованы упрощённые фрагменты кода. Так, например, вы можете заметить, что я часто не использую такие ключевые слова, как override, final, public (когда речь идёт о наследовании), ради большей компактности кода, чтобы он элементарно помещался у вас на экране. Кроме того, я иногда предпочитаю использовать struct вместо class, просто чтобы сэкономить строчку и не писать public:, и опускаю виртуальный деструктор, конструктор, конструктор копирования, префикс std::, намеренно убирая динамическую память. Я считаю себя прагматичным человеком и стремлюсь донести свои мысли самым простым способом, а не умничать, говоря на непонятном языке.

Внимание:

  • Если уже в начале статьи для вас что-то было непонятно (хотя вроде бы ничего сверхсложного), я бы предложил вам прочитать прежде вот этот материал. Думаю, что по его прочтении вам захочется поподробнее изучить эту тему.
  • Весь код, который вы найдёте в этой статье, компилируется с использованием C++20 (хотя я использовал в основном функциональные средства современного C++ вплоть до C++17). Так что, если у вас нет доступа к последнему компилятору, можете воспользоваться https://wandbox.org/, где имеется предварительно установленная библиотека boost.

Цель

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

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

Для чего нам шаблон проектирования прототипов

struct Office {
    string         m_street;
    string         m_city;
    int32_t         m_cubical;    Office(string s, string c, int32_t n):m_street(s), m_city(c), m_cubical(n){}
};struct Employee {
    string      m_name;
    Office        m_office;    Employee(string n,  Office o):m_name(n), m_office(o){}
};int main() {
    Employee john{ "John Doe", Office{"123 East Dr", "London", 123} };
    Employee jane{ "Jane Doe", Office{"123 East Dr", "London", 124} };
    Employee jack{ "jack Doe", Office{"123 ORR", "Bangaluru", 300} };
    return EXIT_SUCCESS;
}
  • Такой подход неправильный, ведь вам приходится прописывать адрес главного офиса для каждого сотрудника. Получается громоздко, и при создании целого списка сотрудников проще не станет. А представьте ситуацию, когда ваш главный офис переехал на другой адрес!

Примеры шаблонов проектирования прототипов на языке C++

  • Более прагматичный подход был бы таким:
struct Employee {
    string          m_name;
    const Office*   m_office;

    Employee(string n,  Office *o):m_name(n), m_office(o){}
};

static Office   LondonOffice{"123 East Dr", "London", 123};
static Office   BangaluruOffice{"RMZ Ecoworld ORR", "London", 123};

int main() {
    Employee john{ "John Doe", &LondonOffice };
    Employee jane{ "Jane Doe", &LondonOffice };
    Employee jack{ "jack Doe", &BangaluruOffice };
    return EXIT_SUCCESS;
}

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

Фабрика прототипов

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

  • Но это не слишком удобно для пользователей вашего API, поэтому надо бы предоставить им прототип, с которым они могли бы работать. И таким образом недвусмысленно дать им понять, что существует единственный унифицированный способ создания экземпляров — из прототипа, а отдельные экземпляры сами по себе создаваться не могут.
  • И в этом случае мы будем использовать фабрику прототипов:
struct Office {
    string      m_street;
    string      m_city;
    int32_t     m_cubical;
};

class Employee {
    string      m_name;
    Office*     m_office;

    // Закрытый конструктор, поэтому прямой экземпляр не может быть создан, кроме `class EmployeeFactory`
    Employee(string n, Office *o) : m_name(n), m_office(o) {}
    friend class EmployeeFactory;

public:
    Employee(const Employee &rhs) : m_name{rhs.m_name}, m_office{new Office{*rhs.m_office}} 
    { }

    Employee& operator=(const Employee &rhs) {
        if (this == &rhs) return *this;
        m_name = rhs.m_name;
        m_office = new Office{*rhs.m_office};
        return *this;
    }

    friend ostream &operator<<(ostream &os, const Employee &o) {
        return os << o.m_name << " works at " 
        << o.m_office->m_street << " " << o.m_office->m_city << " seats @" << o.m_office->m_cubical;
    }
};

class EmployeeFactory {
    static Employee     main;
    static Employee     aux;
    static unique_ptr<Employee> NewEmployee(string n, int32_t c, Employee &proto) {
        auto e = make_unique<Employee>(proto);
        e->m_name = n;
        e->m_office->m_cubical = c;
        return e;
    }

public:
    static unique_ptr<Employee> NewMainOfficeEmployee(string name, int32_t cubical) {
        return NewEmployee(name, cubical, main);
    }
    static unique_ptr<Employee> NewAuxOfficeEmployee(string name, int32_t cubical) {
        return NewEmployee(name, cubical, aux);
    }
};

// Инициализация статического члена 
Employee EmployeeFactory::main{"", new Office{"123 East Dr", "London", 123}};
Employee EmployeeFactory::aux{"", new Office{"RMZ Ecoworld ORR", "London", 123}};

int main() {
    auto jane = EmployeeFactory::NewMainOfficeEmployee("Jane Doe", 125);
    auto jack = EmployeeFactory::NewAuxOfficeEmployee("jack Doe", 123);
    cout << *jane << endl << *jack << endl;
    return EXIT_SUCCESS;
}
/*
Марьиванна работает по адресу 123 East Dr London seats @125
Вася Пупкин работает по адресу RMZ Ecoworld ORR London seats @123
*/
  • Обратите внимание здесь на закрытый конструктор Employee и friend EmployeeFactory. Так мы добиваемся того, чтобы клиент/API-пользователь создавал экземпляр Employee только через EmployeeFactory.

Использование шаблона проектирования прототипов для реализации виртуального конструктора копирования

  • В C++ прототип также используется для создания копии объекта независимо от конкретного его типа. Поэтому его ещё называют виртуальным конструктором копирования.

Проблема

  • C++ поддерживает разрушение полиморфных объектов с помощью виртуального деструктора базового класса. Но соответствующая поддержка создания и копирования объектов отсутствует, так как С++ не поддерживает виртуальный конструктор и виртуальные конструкторы копирования.
  • Более того, вы не можете создать объект, если не знаете его статический тип, потому что компилятор должен знать объём пространства, которое ему нужно выделить. По той же причине для копирования объекта также требуется, чтобы тип объекта был известен во время компиляции.
  • Проиллюстрируем проблему следующим примером:
struct animal {
    virtual ~animal(){ cout<<"~animal\n"; }
};

struct dog : animal {
    ~dog(){ cout<<"~dog\n"; }
};

struct cat : animal {
    ~cat(){ cout<<"~cat\n"; }
};

void who_am_i(animal *who) { // не известно, будет ли здесь собака или кошка
    // Как `создать` объект того же типа, т.е. обозначаемый словом who (кто)?
    // Как `скопировать` объект того же типа, т.е. обозначаемый словом who (кто)?
    delete who; // вы можете удалить соответствующий объект, обозначаемый словом who (кто), благодаря виртуальному деструктору
}
  • И не думайте о dynamic_cast<>: это код с запашком.

Решение

  • Техника виртуального конструктора/конструктора копирования делает возможным полиморфное создание и копирование объектов в C++, делегируя акт создания и копирования объекта производному классу с помощью виртуальных методов.
  • Следующий код реализует не только виртуальный конструктор копирования (т.е. clone()), но и виртуальный конструктор (т. е. create()).
struct animal {
    virtual ~animal() = default;
    virtual std::unique_ptr<animal> create() = 0;
    virtual std::unique_ptr<animal> clone() = 0;
};

struct dog : animal {
    std::unique_ptr<animal> create() { return std::make_unique<dog>(); }
    std::unique_ptr<animal> clone() { return std::make_unique<dog>(*this); }
};

struct cat : animal {
    std::unique_ptr<animal> create() { return std::make_unique<cat>(); }
    std::unique_ptr<animal> clone() { return std::make_unique<cat>(*this); }
};

void who_am_i(animal *who) {
    auto new_who = who->create();// `создать` объект того же типа, т.е. обозначаемый словом who (кто)?
    auto duplicate_who = who->clone(); // `скопировать` объект того же типа, т.е. обозначаемый словом who (кто)?    
    delete who; 
}

Преимущества шаблона проектирования прототипов

  1. Прототипы используются в случаях, когда инстанцирование объекта дорого. Таким образом уходят от дорогостоящего «создания с нуля» в пользу поддержки дешёвого клонирования предварительно инициализированного прототипа.
  2. Прототип обеспечивает гибкость для создания высокодинамичных систем, определяя новое поведение с помощью композиции объектов и указывая значения для переменных-членов объекта во время инстанцирования, в отличие от определения новых классов.
  3. Вы можете упростить систему, создавая сложные объекты более удобным способом.
  4. Шаблон проектирования прототипов используется при создании копии объекта независимо от конкретного его типа, особенно в C++.

Заключение в форме ответов на часто задаваемые вопросы

Вопрос: для чего нужен шаблон проектирования прототипов?

  • Для быстрого создания объекта путём клонирования предварительно настроенного объекта.
  • Чтобы избавиться от кучи лишнего шаблонного кода.
  • Он удобен при работе с объектом независимо от конкретного его типа.
  • Шаблон проектирования прототипов — это очевидный выбор для тех, кто работает с шаблоном проектирования «Команда». Например, в HTTP-запросе большую часть времени содержимое заголовка и подвала остаётся неизменным — меняются только данные. В этом случае создавать объект с нуля не следует. Лучше использовать шаблон проектирования прототипов.

Вопрос: шаблон проектирования прототипов — на самом деле просто клон?

Нет, это не так, если использовать его вместе с фабричным шаблоном проектирования.

Вопрос: шаблон проектирования прототипов используется, когда создавать новый объект дорого, но мы же создаём в clone().

Вы, должно быть, обратили внимание на то, что в разделе статьи, посвящённой фабрике прототипов, мы создаём экземпляры в конструкторе копирования. Разве это дорого? Тогда представьте себе HTTP-запрос: его заголовок состоит из версии, типа кодировки, типа содержимого, типа сервера и т. д. Сначала вам нужно найти эти параметры с помощью вызовов соответствующих функций. Но параметры не меняются, пока соединение не закрыто. Поэтому есть ли смысл снова и снова выполнять вызовы функций для получения этих параметров? Дорого обходятся нам не параметры, а их функции для получения значения.

Читайте также:


Перевод статьи Vishal Chovatiya: Prototype Design Pattern in Modern C++

Предыдущая статьяМетод подсчёта количества решений
Следующая статьяЧто такое Tailwind CSS и как внедрить его на сайт или в React-приложение?