Skip to content

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

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

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

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

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

Задача

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

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

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

Идея

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

  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. Когда создание объекта разнесено в коде, объект создается не сразу (например, данные подготавливаются поэтапно)
  • Преимущество: вынесение создания и контроля в отдельные классы
  • Проблема: с данными - конкретные строители базируются на одних и тех же входных данных и количество этапов строительства не меняется ⇒ возникнут проблемы при подмене одного строителя на другой).
  • Решение: сделать агрегацию не на уровне базовых классов, а на уровне производных. Тогда можно будет менять интерфейс базового билдера (в том числе расширять или сужать), и конкретный директор будет работать с конкретным билдером не как с базовым классом, а как с производным.
Clone this wiki locally