Skip to content

ООП 12. Порождающие паттерны: фабричный метод (Factory Method), абстрактная фабрика (Abstract Factory), строитель (Builder). Их преимущества и недостатки.

Kozlitin Maxim edited this page Jun 14, 2023 · 2 revisions

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

Преимущества использования паттернов:

  • Мы имеем готовое решение
  • За счет готового решения - нюансы все выявлены => надежный код
  • Повышается скорость разработки
  • Повышается читаемость кода
  • Улучшается взаимодействие с коллегами (достаточно сказать название паттерна, который вы используете, и всё сразу станет понятно)

Задача

Задача порождающего паттерна: создание объектов

Проблема: не может быть полиморфных конструкторов. Если внутри метода мы работаем со ссылкой/указателем на базовое понятие, будет вызван конструктор базового понятия.

image

Одиночка (Singleton)

Возникают задачи, в которых должно быть гарантировано, что создан только один объект класса.

Решение: убрать конструкторы из public части. В паблик части сделать статический метод, который будет при необходимости порождать объект. В статическом методе содержится статический член класса своего объекта. Поскольку он статический – будет создан только один раз. Конструктор находится в private части. Нельзя копировать – запрещаем конструктор копирования и оператор присваивания.

Недостатки метода:

  • Глобальный объект: доступ через глобальный интерфейс, вызовом статического метода
  • Проблема шаблона: лишаемся подмены. Решение о том, какой объект создавать, принимается на этапе компиляции. Шаблоны здесь лучше не использовать.

Альтернатива:

  • фабричный метод.

  • Пример кода. Singleton обычный (Запрещаем конструктор копирования и оператор присваивания)

    # include <iostream>
    # include <memory>
    
    using namespace std;
    
    class Product
    {
    public:
       static shared_ptr<Product> instance()
       {
           static shared_ptr<Product> myInstance(new Product());
    
           return myInstance;
       }
       ~Product() { cout << "Destructor;" << endl; }
    
       void f() { cout << "Method f;" << endl; }
    
       Product(const Product&) = delete; // запрещаем
       Product& operator=(const Product&) = delete; // запрещаем
    
    private:
       Product() { cout << "Default constructor;" << endl; }
    };
    
    int main()
    {
       shared_ptr<Product> ptr(Product::instance());
    
       ptr->f();
    }
  • Пример кода. Singleton шаблонный (Запрещаем конструктор копирования и оператор присваивания)

    # include <iostream>
    # include <memory>
    
    using namespace std;
    
    template < Type>
    class Singleton
    {
    public:
        static Type& instance()
        {
            static unique_ptr<Type> myInstance(new Type());
    
            return *myInstance;
        }
    
        Singleton() = delete;
        Singleton(const Singleton<Type>&) = delete;
        Singleton<Type>& operator=(const Singleton<Type>&) = delete;
    };
    
    class Product
    {
    public:
        Product() { cout << "Default constructor;" << endl; }
        ~Product() { cout << "Destructor;" << endl; }
    
        void f() { cout << "Method f;" << endl; }
    };
    
    int main()
    {
        Product& d = Singleton<Product>::instance();
    
        d.f();
    }

Фабричный метод

Идея

Разнести на две задачи:

  1. Принятие решения, какой объект создавать.
  2. Создание объекта, причем при создании объекта нужно "отвязаться" от конкретного типа.

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

Диаграмма

image-2

Использование

Когда нам нужно использовать фабричный метод:

  1. Основная задача: подмена одного объекта на другой.
  2. Когда принятие решение в одном месте кода, создание - в другом.

Преимущества

  • Облегчается добавление новых классов, избавляем методы от привязки к конкретным классам.

  • Код очищается от new, используя полиморфизм по полной (создаём новые классы, не изменяя уже написанный код)

  • Паттерн работает во всех языках

  • Позволяет разнести в коде принятие решения о создании объекта (solution) и само создание (creator)

  • Решение о том, какой объект создавать, принимается во время выполнения, тогда же можно менять это решение

  • Пример. Фабричный метод (Factory Method). Новый объект.

    #include <iostream>
    #include <memory>
    
    using namespace std;
    
    class Product;
    
    class Creator {
     public:
        virtual unique_ptr<Product> createProduct() = 0;
    };
    
    template <typename Tprod>
    class ConCreator : public Creator {
     public:
        virtual unique_ptr<Product> createProduct() override {
            return unique_ptr<Product>(new Tprod());
        }
    };
    
    class Product {
     public:
        virtual ~Product() = 0;
        virtual void run() = 0;
    };
    
    Product::~Product() {}
    
    class ConProd1 : public Product {
     public:
        virtual ~ConProd1() override { cout << "Destructor;" << endl; }
        virtual void run() override { cout << "Method run;" << endl; }
    };
    
    #pragma endregion
    
    int main() {
        shared_ptr<Creator> cr(new ConCreator<ConProd1>());
        shared_ptr<Product> ptr = cr->createProduct();
    
        ptr->run();
    }
  • Пример. Фабричный метод (Factory Method). Без повторного создания.

    # include <iostream>
    # include <memory>
    
    using namespace std;
    
    class Product;
    
    class Creator
    {
    public:
        shared_ptr<Product> getProduct();
    
    protected:
        virtual shared_ptr<Product> createProduct() = 0;
    
    private:
        shared_ptr<Product> product;
    };
    
    template <typename Tprod>
    class ConCreator : public Creator
    {
    protected:
        virtual shared_ptr<Product> createProduct() override
        {
            return shared_ptr<Product>(new Tprod());
        }
    };
    
    shared_ptr<Product> Creator::getProduct()
    {
        if (!product)
        {
            product = createProduct();
        }
    
        return product;
    }
    
    class Product
    {
    public:
        virtual ~Product() = 0;
        virtual void run() = 0;
    };
    
    Product::~Product() {}
    
    class ConProd1 : public Product
    {
    public:
        virtual ~ConProd1() override { cout << "Destructor;" << endl; }
        virtual void run() override    { cout << "Method run;" << endl; }
    };
    	
    int main()
    {
        shared_ptr<Creator> cr(new  ConCreator<ConProd1>());
        shared_ptr<Product> ptr1 = cr->getProduct();
        shared_ptr<Product> ptr2 = cr->getProduct();
    
        cout << ptr1.use_count() << endl;
        ptr1->run();
    }
  • Solution

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

    Solution предоставляет метод для регистрации creator-ов.

image-3

На основе чего Solution может принять решение, какой класс создавать? Solution должен быть независим от реализации, от конкретного набора классов - следовательно, **мы не можем использовать switch-case.**

**Идея решения:** 

создаем карту продуктов, которые у нас существуют. При добавлении нового класса, регистрируем его в этой карте. Используя эту карту, осуществляем выбор. **Solution предоставляет метод для регистрации креаторов классов (в этой карте).**

- Пример. Фабричный метод (Factory Method). Разделение обязанностей.
    
    Solution предоставляет метод для регистрации (в данном случае) Creator'ов. В данном случае для карты - map, состоящий из пар (pair): ключ + значение. Таким образом мы избавились от конструкции switch.
    
    - Код
        
        ```cpp
        # include <iostream>
        # include <memory>
        # include <map>
        
        using namespace std;
        
        class Product;
        
        class Creator
        {
        public:
            virtual unique_ptr<Product> createProduct() = 0;
        };
        
        template <typename Tprod>
        class ConCreator : public Creator
        {
        public:
            virtual unique_ptr<Product> createProduct() override
            {
                return unique_ptr<Product>(new Tprod());
            }
        }; 
        
        #pragma region Product
        class Product
        {
        public:
            virtual ~Product() = 0;
            virtual void run() = 0;
        };
        
        Product::~Product() {}
        
        class ConProd1 : public Product
        {
        public:
            virtual ~ConProd1() override { cout << "Destructor;" << endl; }
            virtual void run() override { cout << "Method run;" << endl; }
        };
        
        unique_ptr<Creator> createConCreator()
        {
            return unique_ptr<Creator>(new ConCreator<ConProd1>());
        }
        
        class Solution
        {
        public:
            typedef unique_ptr<Creator> (*CreateCreator)();
        
            bool registration(size_t id, CreateCreator createfun)
            {
                return callbacks.insert(CallBackMap::value_type(id, createfun)).second;
            }
            bool check(size_t id) { return callbacks.erase(id) == 1; }
        
            unique_ptr<Creator> create(size_t id)
            {
                CallBackMap::const_iterator it = callbacks.find(id);
        
                if (it == callbacks.end())
                {
                    throw IdError();
                }
        
                return unique_ptr<Creator>((it->second)());
            }
        
        private:
            using CallBackMap = map<size_t, CreateCreator>;
        
            CallBackMap callbacks;
        };
        
        int main()
        {
            Solution solution;
        
            solution.registration(1, createConCreator);
        
            shared_ptr<Creator> cr(solution.create(1));
            shared_ptr<Product> ptr = cr->createProduct();
        
            ptr->run();
        }
        ```

Абстрактная фабрика

Абстрактная фабрика - развитие фабричного метода с добавлением функционала

  • Задача: создание "семейства" разных объектов, но связанных между собой

    Можно "плодить" разные ветви Creаtor'ов под каждый тип. продуктов, но мы теряем связь между этими продуктами.

Пример: в графических библиотеках: кисточка, ручка, сцена и т. п.

image-4

Каждая конкретная фабрика будет отвечать за создание определенного семейства объектов

Как и для фабричного метода, должен быть solution, который принимает решение, какую фабрику создавать

  • Преимущества: не надо контролировать создание каждого объекта - только всего семейства целиком. Целостность системы.

  • Недостаток: абстрактная фабрика накладывает требования на продукты (в разных семействах должны быть представлены все продукты, которые определяет базовая абстрактная фабрика). Очень сложно привести всё к единому интерфейсу.

  • Пример кода. Абстрактная фабрика (Abstract Factory).

    # include <iostream>
    # include <memory>
    
    using namespace std;
    
    class Image {};
    class Color {};
    
    class BaseGraphics 
    {
    public:	virtual ~BaseGraphics() = 0;
    };
    BaseGraphics::~BaseGraphics() {}
    
    class BasePen {};
    class BaseBrush {};
    
    class QtGraphics : public BaseGraphics
    {
    public:
    	QtGraphics(shared_ptr<Image> im) { cout << "Constructor QtGraphics;" << endl; }
    	virtual ~QtGraphics() override { cout << "Destructor QtGraphics;" << endl; }
    };
    
    class QtPen : public BasePen {};
    class QtBrush : public BaseBrush {};
    
    class AbstractGraphFactory
    {
    public:
    	virtual unique_ptr<BaseGraphics> createGraphics(shared_ptr<Image> im) = 0;
    	virtual unique_ptr<BasePen> createPen(shared_ptr<Color> cl) = 0;
    	virtual unique_ptr<BaseBrush> createBrush(shared_ptr<Color> cl) = 0;
    };
    
    class QtGraphFactory : public AbstractGraphFactory
    {
    	virtual unique_ptr<BaseGraphics> createGraphics(shared_ptr<Image> im)
    	{ return unique_ptr<BaseGraphics>(new QtGraphics(im)); }
    
    	virtual unique_ptr<BasePen> createPen(shared_ptr<Color> cl)
    	{ return unique_ptr<BasePen>(new QtPen()); }
    
    	virtual unique_ptr<BaseBrush> createBrush(shared_ptr<Color> cl)
    	{ return unique_ptr<BaseBrush>(new QtBrush()); }
    };
    
    int main()
    {
    	shared_ptr<AbstractGraphFactory> grfactory(new QtGraphFactory());
    
    	shared_ptr<BaseGraphics> graphics1 = grfactory->createGraphics(shared_ptr<Image>(new Image()));
    	shared_ptr<BaseGraphics> graphics2 = grfactory->createGraphics(shared_ptr<Image>(new Image()));
    }

Строитель

Проблема: сложные объекты создаются поэтапно, иногда - этапы создания разнесены в разных частях программы. 

Идея решения: вынести в отдельный код этапы создания сложных объектов.

  • Строитель - класс, который включает в себя этапы создания сложного объекта.
  • Кроме того, выделяем еще один класс, который контролирует создание - Директор.
  • Строитель - создает объект.
  • Директор - подготавливает данные для создания, контролирует этапы создания, отдает объект клиенту.

Диаграмма (должен быть ещё производный директор ConDirector):

image-5

  • Пример кода. Строитель

    # include <iostream>
    # include <memory>
    
    using namespace std;
    
    class Product
    {
    public:
        Product() { cout << "Default constructor;" << endl; }
        ~Product() { cout << "Destructor;" << endl; }
    
        void run() { cout << "Method run;" << endl; }
    };
    
    class Builder
    {
    public:
        virtual bool buildPart1() = 0;
        virtual bool buildPart2() = 0;
    
        shared_ptr<Product> getProduct();
    
    protected:
        virtual shared_ptr<Product> createProduct() = 0;
    
        shared_ptr<Product> product;
    };
    
    class ConBuilder : public Builder
    {
    public:
        virtual bool buildPart1() override { cout << "Completed part: " << ++part << ";" << endl; return true; }
        virtual bool buildPart2() override { cout << "Completed part: " << ++part << ";" << endl; return true; }
    
    protected:
        virtual shared_ptr<Product> createProduct() override;
    
    private:
        size_t part{0};
    };
    
    class Director
    {
    public:
        shared_ptr<Product> create(shared_ptr<Builder> builder)
        {
            if (builder->buildPart1() && builder->buildPart2()) return builder->getProduct();
    
            return shared_ptr<Product>();
        }
    };
    
    shared_ptr<Product> Builder::getProduct()
    {
        if (!product) { product = createProduct(); }
    
        return product;
    }
    
    shared_ptr<Product> ConBuilder::createProduct()
    {
        if (part == 2) { product = shared_ptr<Product>(new Product()); }
    
        return product;
    }
     
    int main()
    {
        shared_ptr<Builder> builder(new ConBuilder());
        shared_ptr<Director> director(new Director());
    
        shared_ptr<Product> prod = director->create(builder);
    
        if (prod)
            prod->run();
    }

Как и для фабричного метода, должен быть solution, который принимает решение, какого директора создавать

  • Когда надо использовать?
    1. Для поэтапного создания сложных объектов
    2. Когда создание объекта разнесено в коде, объект создается не сразу (например, данные подготавливаются поэтапно)
  • Преимущество: вынесение создания и контроля в отдельные классы
  • Проблема: с данными - конкретные строители базируются на одних и тех же входных данных и количество этапов строительства не меняется ⇒ возникнут проблемы при подмене одного строителя на другой).
  • Решение: сделать агрегацию не на уровне базовых классов, а на уровне производных. Тогда можно будет менять интерфейс базового билдера (в том числе расширять или сужать), и конкретный директор будет работать с конкретным билдером не как с базовым классом, а как с производным.

Прототип

  • Проблема: Хотим создать копию объекта, не зная его класса. (Например, в подменяемом методе). Также мы не хотим тащить за ним его Creator’ы.
  • Решение: Добавляем в базовый класс метод clone(), который создаст новый объект на основе существующего. Производные классы реализуют clone() под себя.

image-6

  • Пример кода. Prototype

    # include <iostream>
    # include <memory>
    
    using namespace std;
    
    class BaseObject
    {
    public:
    	virtual ~BaseObject() = default;
    
    	virtual unique_ptr<BaseObject> clone() = 0;
    };
    
    class Object1 : public BaseObject
    {
    public:
    	Object1() { cout << "Default constructor;" << endl; }
    	Object1(const Object1& obj) { cout << "Copy constructor;" << endl; }
    	~Object1() { cout << "Destructor;" << endl; }
    
    	virtual unique_ptr<BaseObject> clone() override
    	{
    		return unique_ptr<BaseObject>(new Object1(*this));
    	}
    };
    
    int main()
    {
    	unique_ptr<BaseObject> ptr1(new Object1());
    
    	auto ptr2 = ptr1->clone();
    }

Преимущества: очень удобно использовать, когда объект сложно создаётся.

Пул объектов

Пул объектов предоставляет набор готовых объектов, которые мы можем использовать

Пример использования

Многопроцессорная система. Количество заданий, которое мы должны создать, должно быть не больше количества процессоров. Чтобы каждый процессор выполнял свою задачу.

Когда надо использовать?

Когда создание или уничтожение какого-либо объекта - трудоемкий процесс и надо "держать" определенное количество объектов в системе.

Задачи

  1. Он держит эти объекты.
  2. Может их создавать (то есть может расширяться).
  3. По запросу отдает объект.
  4. Если клиенту этот объект не нужен, он может его вернуть в пул.

Исходя из пунктов 3 и 4, для каждого включенного в пул объекта мы должны установить, используется он или не используется.

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

Пул объектов можно с помощью одиночки – чтобы пул объектов был только один. Пул объектов – контейнерный класс, для него используем итератор. Необходимо знать, занят объект или нет, используем пару: ключ (bool) и объект.

  • Диаграмма

image-7

  • Пример кода. ObjectPool

    #include <iostream>
    #include <iterator>
    #include <memory>
    #include <vector>
    
    using namespace std;
    
    class Product {
     private:
        static size_t count;
    
     public:
        Product() { cout << "Constructor(" << ++count << ");" << endl; }
        ~Product() { cout << "Destructor(" << count-- << ");" << endl; }
    
        void clear() { cout << "Method clear: 0x" << this << endl; }
    };
    
    size_t Product::count = 0;
    
    template <typename Type>
    class ObjectPool {
     public:
        static shared_ptr<ObjectPool<Type>>
        instance();  // статический метод из одиночки
    
        shared_ptr<Type> getObject();
        bool releaseObject(shared_ptr<Type>& obj);
        size_t count() const { return pool.size(); }
    
        iterator<output_iterator_tag, const pair<bool, shared_ptr<Type>>> begin()
            const;
        iterator<output_iterator_tag, const pair<bool, shared_ptr<Type>>> end()
            const;
    
        ObjectPool(const ObjectPool<Type>&) = delete;
        ObjectPool<Type>& operator=(const ObjectPool<Type>&) = delete;
    
     private:
        vector<pair<bool, shared_ptr<Type>>> pool;
    
        ObjectPool() {}
    
        pair<bool, shared_ptr<Type>> create();
    
        template <typename Type>
        friend ostream& operator<<(ostream& os, const ObjectPool<Type>& pl);
    };
    
    template <typename Type>
    shared_ptr<ObjectPool<Type>> ObjectPool<Type>::instance() {
        static shared_ptr<ObjectPool<Type>> myInstance(new ObjectPool<Type>());
    
        return myInstance;
    }
    
    template <typename Type>
    shared_ptr<Type> ObjectPool<Type>::getObject() {
        size_t i;
        for (i = 0; i < pool.size() && pool[i].first; ++i)
            ;
    
        if (i < pool.size()) {
            pool[i].first = true;
        } else {
            pool.push_back(create());
        }
    
        return pool[i].second;
    }
    
    template <typename Type>
    bool ObjectPool<Type>::releaseObject(shared_ptr<Type>& obj) {
        size_t i;
        for (i = 0; i < pool.size() && pool[i].second != obj; ++i)
            ;
    
        if (i == pool.size()) return false;
    
        obj.reset();
        pool[i].first = false;
        pool[i].second->clear();
    
        return true;
    }
    
    template <typename Type>
    pair<bool, shared_ptr<Type>> ObjectPool<Type>::create() {
        return pair<bool, shared_ptr<Type>>(true, shared_ptr<Type>(new Type()));
    }
    
    template <typename Type>
    ostream& operator<<(ostream& os, const ObjectPool<Type>& pl) {
        for (auto elem : pl.pool)
            os << "{" << elem.first << ", 0x" << elem.second << "} ";
    
        return os;
    }
    
    int main() {
        shared_ptr<ObjectPool<Product>> pool = ObjectPool<Product>::instance();
    
        vector<shared_ptr<Product>> vec(4);
    
        for (auto& elem : vec) elem = pool->getObject();
    
        pool->releaseObject(vec[1]);
    
        cout << *pool << endl;
    
        shared_ptr<Product> ptr = pool->getObject();
        vec[1] = pool->getObject();
    
        cout << *pool << endl;
    }

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

  • Пул объектов - контейнерный класс, удобно использовать итераторы!
  • Необходимо знать, занят объект или нет, используем пару: ключ (bool) и объект.
Clone this wiki locally