Обращение зависимостей и порождающие шаблоны проектирования

    Аннотация


    Это третья статья, просвещенная порождающим шаблонам проектирования и связанным с ними вопросами. Здесь мы рассмотрим излюбленные приемы при создании объектов: фабрики, заводы, абстрактные фабрики, строители, прототипы, мультитоны, отложенные инициализации, а также немного коснемся pimpl идиомы или шаблона “мост”. Использование синглтонов было подробно рассмотрено в первой [1] и второй [2] статьях, однако, как вы увидите в дальнейшем, синглтоны часто используются совместно с другими шаблонами проектирования.


    Введение


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

    Не секрет, что многие, узнав о шаблонах, стараются начать их повсеместно использовать. Однако не все так просто. Многие статьи этой тематики не уделяют должного внимания их использованию в коде. А когда начинают прикручивать шаблоны к коду, тут возникает нечто такое невообразимое, что ни в сказке сказать, ни пером описать. Мне доводилось видеть различные воплощения этих идей, иногда невольно задаешься вопросом: а что курил автор? Взять, к примеру, фабрику или фабричный метод из Википедии [3]. Я не буду приводить весь код, приведу лишь использование:

    const size_t count = 2;
    // An array of creators
    Creator* creators[count] = { new ConcreteCreatorA(), new ConcreteCreatorB() };
    
    // Iterate over creators and create products
    for (size_t i = 0; i < count; i++) {
        Product* product = creators[i]->factoryMethod();
        cout << product->getName() << endl;
        delete product;
    }
    
    for (size_t i = 0; i < count; i++)
        delete creators[i];
    

    Если себя спросить, а как это использовать в реальной жизни, то сразу возникают следующие замечания:
    1. Как я узнаю, что мне надо использовать именно 0-й или 1-й элемент? Они ничем не отличаются.
    2. Предположим, что надо в цикле создать некие элементы. Откуда я возьму знание о том, где находятся эти фабрики? Если я фабрики инициализирую тут же, то зачем мне они вообще нужны? Можно просто создать объект и позвать определенный метод или отдельно стоящую функцию, которая все сделает.
    3. Предполагается, что объекты создаются оператором new. Тут сразу возникает вопрос с обработкой исключительных ситуаций и времени жизни объекта.

    Как ни крути, а данный пример лишь некая иллюстрация, которая содержит множество изъянов. В реальной жизни такое не используют.

    «А что же используют тогда?», спросит внимательный читатель. Ниже приведен код использования. Этот список не претендует на полноту:
    // создание объекта, используя фабрику, получаемую из синглтона
    Object* o = Factory::getInstance().createObject("object_name");
    
    // использование конфигурации для создания объектов
    Configuration* conf = Configuration::getCurrentConfiguration();
    Object* o = Factory::getInstance().createObject(conf->getObjectNameToCreate());
    

    Стоит обратить внимание, что фабрики в «реальной жизни» как правило являются синглтонами. Также можно заметить, что при создании объектов «торчат уши» использованных шаблонов. При последующем рефакторинге это даст о себе знать с неприятной стороны. Часто используется подход, когда возвращаются объекты по указателю. Так учили во всех книжках, так продолжается писаться код. Если с методом createObject все ясно — надо позвать delete в конце, то что делать с конфигурацией? Это синглтон или нет? Если да, то ничего делать не нужно. А если нет? Возникают опять вопросы с временем жизни. Про правильную обработку исключений тоже не надо забывать, и такой код с обработкой исключительных ситуаций вызывает проблемы, связанные с чисткой ресурсов.

    Как ни крути, а хотелось бы иметь единый подход, который бы проходил красной нитью сквозь порожденные объекты и не отличал различные способы создания, коих множество. Для того, чтобы претворить это в жизнь, будем использовать мощный принцип обращения зависимостей [7]. Суть его состоит в том, что вводится некая абстракция, интерфейс. Далее использующий и используемый код связывается посредством введенного интерфейса используя, например, обращение контроля [8]. Это позволяет коду, который хочет создавать объекты, абстрагироваться от конкретики создания класса и просто использовать выделенный интерфейс. Вся забота ложится на плечи функционала, реализующего этот интерфейс. В статье подробно рассмотрены способы создания объектов с использованием практически всех известных порождающих шаблонов проектирования, а также приведен пример, когда для создания экземпляров используется несколько порождающих шаблонов одновременно. Пример синглтона подробно описан в предыдущей статье [2], в этой статье будет лишь его использование совместно в другими шаблонами.

    Инфраструктура


    Объект An и инфраструктура вокруг него подробно описана во второй статье [2]. Здесь я лишь приведу код, который будет использоваться в дальнейшем повествовании. Для уточнения деталей смотрите предыдущую статью [2].

    template<typename T>
    struct An
    {
        template<typename U>
        friend struct An;
    
        An()                              {}
    
        template<typename U>
        An(const An<U>& a) : data(a.data) {}
    
        template<typename U>
        An(An<U>&& a) : data(std::move(a.data)) {}
    
        T* operator->()                   { return get0(); }
        const T* operator->() const       { return get0(); }
        bool isEmpty() const              { return !data; }
        void clear()                      { data.reset(); }
        void init()                       { if (!data) reinit(); }
        void reinit()                     { anFill(*this); }
        
        T& create()                       { return create<T>(); }
    
        template<typename U>
        U& create()                       { U* u = new U; data.reset(u); return *u; }
        
        template<typename U>
        void produce(U&& u)               { anProduce(*this, u); }
    
        template<typename U>
        void copy(const An<U>& a)         { data.reset(new U(*a.data)); }
    
    private:
        T* get0() const
        {
            const_cast<An*>(this)->init();
            return data.get();
        }
    
        std::shared_ptr<T> data;
    };
    
    template<typename T>
    void anFill(An<T>& a)
    {
        throw std::runtime_error(std::string("Cannot find implementation for interface: ")
                + typeid(T).name());
    }
    
    template<typename T>
    struct AnAutoCreate : An<T>
    {
        AnAutoCreate()     { create(); }
    };
    
    template<typename T>
    T& single()
    {
        static T t;
        return t;
    }
    
    template<typename T>
    An<T> anSingle()
    {
        return single<AnAutoCreate<T>>();
    }
    
    #define PROTO_IFACE(D_iface, D_an)    \
        template<> void anFill<D_iface>(An<D_iface>& D_an)
    
    #define DECLARE_IMPL(D_iface)    \
        PROTO_IFACE(D_iface, a);
    
    #define BIND_TO_IMPL(D_iface, D_impl)    \
        PROTO_IFACE(D_iface, a) { a.create<D_impl>(); }
    
    #define BIND_TO_SELF(D_impl)    \
        BIND_TO_IMPL(D_impl, D_impl)
    
    #define BIND_TO_IMPL_SINGLE(D_iface, D_impl)    \
        PROTO_IFACE(D_iface, a) { a = anSingle<D_impl>(); }
    
    #define BIND_TO_SELF_SINGLE(D_impl)    \
        BIND_TO_IMPL_SINGLE(D_impl, D_impl)
    
    #define BIND_TO_IFACE(D_iface, D_ifaceFrom)    \
        PROTO_IFACE(D_iface, a) { anFill<D_ifaceFrom>(a); }
    
    #define BIND_TO_PROTOTYPE(D_iface, D_prototype)    \
        PROTO_IFACE(D_iface, a) { a.copy(anSingle<D_prototype>()); }
    

    Если описать вкратце, то объект An представляет из себя «умный» указатель, который автоматически заполняется при обращении к нему используя функцию anFill. Мы будем перегружать эту функцию для нужного нам интерфейса. Для создания объекта на основе входных данных используется функция anProduce, использование которой будет описано в разделе, посвященный фабрикам.

    Шаблон «мост»


    Начнем с самого простого и распространенного случая: скрыть данные объекта, оставив для использования только интерфейс. Таким образом, при изменении данных, например, добавлении одного поля в класс, нет необходимости перекомпилировать все, что использует этот класс. Этот шаблон проектирования называется «мост», еще говорят об pimpl-идиоме. Этот подход часто используется для разделения интерфейса от реализации.

    // header file
    
    // базовый класс всех интерфейсов
    struct IObject
    {
        virtual ~IObject() {}
    };
    
    struct IFruit : IObject
    {
        virtual std::string getName() = 0;
    };
    // декларация заливки реализации для класса IFruit
    DECLARE_IMPL(IFruit)
    
    // cpp file
    struct Orange : IFruit
    {
        virtual std::string getName()
        {
            return "Orange";
        }
    };
    // связывание интерфейса IFruit с реализацией Orange
    BIND_TO_IMPL(IFruit, Orange)
    

    Первым делом создадим класс IObject, чтобы в каждом абстрактном классе не писать виртуальный деструктор. Дальше просто наследуем каждый интерфейс (абстракный класс) от нашего IObject. Интерфейс IFruit содержит единственную функцию getName() в качестве иллюстрации подхода. Вся декларация происходит в заголовочном файле. Конкретная реализация записывается уже в cpp файле. Здесь мы определяем нашу функцию getName() и затем связываем наш интерфейс с реализацией. При изменении изменении класса Orange достаточно перекомпилировать один файл.

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

    An<IFruit> f;
    std::cout << "Name: " << f->getName() << std::endl;
    
    // output
    Name: Orange
    

    Здесь мы просто создаем объект An, а затем при первоначальном доступе создается объект с нужной реализацией, которая описана в файле cpp. Время жизни контролируется автоматически, т.е. по выходу из функции объект автоматически уничтожится.

    Шаблон «фабрика»


    Теперь поговорим о наиболее распространенном шаблоне: фабричный метод или просто фабрика. Здесь я не буду приводить примеров, как обычно используется фабрика. Вы это можете почитать, например, в Википедии. Я собираюсь показать несколько иное использование этого шаблона проектирования.

    Различие использования состоит в том, что для пользователя он остается невидимым в большинстве случаев. Но это не означает, что будут иметь место какие-либо ограничения. В статье будет продемонстрирована гибкость и сила предлагаемого подхода.

    Для этого поставим задачу: необходимо создавать различные объекты в зависимости от входных параметров функции. Производящая функция, вообще говоря, может иметь несколько параметров. Однако без огранчения общности можно считать, что любую функцию с несколькими параметрами можно свести к функции с одним параметром, где в качестве аргумента используется структура с необходимыми входными данными. Поэтому мы везде и всюду будем использовать функцию с одним параметром для упрощения интерфейсов и понимания. Интересующиеся могут использовать variadic templates из нового стандарта c++0x, правда компиляторы msvc и icc их, к сожалению, пока не поддерживают.

    Итак, перед нами стоит задача создать реализацию интерфейса IFruit в зависимости от типа фрукта FruitType:

    enum FruitType
    {
        FT_ORANGE,
        FT_APPLE
    };
    

    Для этого нам потребуется дополнительная реализация для Apple:

    // cpp file
    struct Apple : IFruit
    {
        virtual std::string getName()
        {
            return "Apple";
        }
    };
    

    Создаем производящую функцию:

    void anProduce(An<IFruit>& a, FruitType type)
    {
        switch (type)
        {
        case FT_ORANGE:
            a.create<Orange>();
            break;
    
        case FT_APPLE:
            a.create<Apple>();
            break;
            
        default:
            throw std::runtime_error("Unknown fruit type");
        }
    }
    

    Данная функция автоматически вызывается при вызове метода An::produce, как показано ниже:

    An<IFruit> f;
    f.produce(FT_ORANGE);
    std::cout << f->getName() << std::endl;
    f.produce(FT_APPLE);
    std::cout << f->getName() << std::endl;
    
    // output:
    Orange
    Apple
    

    Часто бывает полезно создавать объекты в завимости не от значений времени исполнения, т.е. в каждый конкретный момент времени мы явно знаем, какой объект хотим создать. Тогда можно использовать другие, более простые способы создания. Первый способ заключается в создании промежуточных объектов — тегов:

    // теги для реализаций в заголовочном файле
    struct OrangeTag {};
    struct AppleTag {};
    
    // реализация производящих функций в cpp файле
    void anProduce(An<IFruit>& a, OrangeTag)
    {
        a.create<Orange>();
    }
    
    void anProduce(An<IFruit>& a, AppleTag)
    {
        a.create<Apple>();
    }
    
    // использование
    An<IFruit> f;
    f.produce(AppleTag());
    std::cout << f->getName() << std::endl;
    f.produce(OrangeTag());
    std::cout << f->getName() << std::endl;
    
    // output
    Apple
    Orange
    

    Второй вариант заключается в создании специальных интерфейсов и использовании шаблона «мост»:

    // header file
    struct IOrange : IFruit {};
    DECLARE_IMPL(IOrange)
    
    struct IApple : IFruit {};
    DECLARE_IMPL(IApple)
    
    // cpp file
    struct Orange : IOrange
    {
        virtual std::string getName()
        {
            return "Orange";
        }
    };
    BIND_TO_IMPL(IOrange, Orange);
    
    struct Apple : IApple
    {
        virtual std::string getName()
        {
            return "Apple";
        }
    };
    BIND_TO_IMPL(IApple, Apple);
    
    // использование
    An<IOrange> o;
    std::cout << "Name: " << o->getName() << std::endl;
    An<IApple> a;
    std::cout << "Name: " << a->getName() << std::endl;
    
    // output
    Name: Orange
    Name: Apple
    


    Шаблон «строитель»


    Многие (в том числе и я) недоумевают, зачем нужен строитель, когда есть фабрика? Ведь по сути, это похожие шаблоны. Чем же они тогда отличаются?

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

    // header file
    struct Fruit
    {
        Fruit(const std::string& name) : m_name(name) {}
        std::string getName() { return m_name; }
        
    private:
        std::string m_name;
    };
    
    // cpp file
    struct Orange : Fruit
    {
        Orange() : Fruit("Orange") {}
    };
    
    struct Apple : Fruit
    {
        Apple() : Fruit("Apple") {}
    };
    
    enum FruitType
    {
        FT_ORANGE,
        FT_APPLE
    };
    
    void anProduce(An<Fruit>& a, FruitType type)
    {
        switch (type)
        {
        case FT_ORANGE:
            a.create<Orange>();
            break;
        case FT_APPLE:
            a.create<Apple>();
            break;
            
        default:
            throw std::runtime_error("Unknown fruit type");
        }
    }
    

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

    An<Fruit> f;
    f.produce(FT_ORANGE);
    std::cout << f->getName() << std::endl;
    f.produce(FT_APPLE);
    std::cout << f->getName() << std::endl;
    
    // output
    Orange
    Apple
    


    Шаблон «абстрактная фабрика»


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

    Предположим, что нам необходимо создавать объекты GUI:

    struct IWindow : IObject
    {
        virtual std::string getWindowName() = 0;
    };
    
    struct IButton : IObject
    {
        virtual std::string getButtonName() = 0;
    };
    

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

    struct IWindowsManager : IObject
    {
        virtual void produceWindow(An<IWindow>& a) = 0;
        virtual void produceButton(An<IButton>& a) = 0;
    };
    

    Теперь декларируем реализации:

    struct GtkWindow : IWindow
    {
        virtual std::string getWindowName()
        {
            return "GtkWindow";
        }
    };
    
    struct GtkButton : IButton
    {
        virtual std::string getButtonName()
        {
            return "GtkButton";
        }
    };
    
    struct GtkWindowsManager : IWindowsManager
    {
        virtual void produceWindow(An<IWindow>& a)    { a.create<GtkWindow>(); }
        virtual void produceButton(An<IButton>& a)    { a.create<GtkButton>(); }
    };
    BIND_TO_IMPL_SINGLE(IWindowsManager, GtkWindowsManager)
    

    И создаем производящие функции:

    PROTO_IFACE(IWindow, a)
    {
        An<IWindowsManager> pwm;
        pwm->produceWindow(a);
    }
    
    PROTO_IFACE(IButton, a)
    {
        An<IWindowsManager> pwm;
        pwm->produceButton(a);
    }
    

    Теперь можно использовать наши интерфейсы:

    An<IButton> b;
    std::cout << b->getWindowName() << std::endl;
    An<IWindow> w;
    std::cout << w->getButtonName() << std::endl;
    
    // output
    GtkButton
    GtkWindow
    

    Усложним пример. Допустим, нам необходимо выбирать фреймворк в зависимости от конфигурации. Смотрим, как это можно реализовать:

    enum ManagerType
    {
        MT_GTK,
        MT_UNKNOWN
    };
    
    // наша конфигурация
    struct Configuration
    {
        // по умолчанию используем неизвестный фреймворк
        Configuration() : wmType(MT_UNKNOWN) {}
        
        ManagerType wmType;
    };
    // связываем конфигурацию с единственным экземпляром (синглтоном)
    BIND_TO_SELF_SINGLE(Configuration)
    
    // класс создает нужные фабрики объектов в зависимости от конфигурации
    struct WindowsManager
    {
        // прописываем явные зависимости от синглтонов, см [1]
        An<IWindowsManager> aWindowsManager;
        An<Configuration> aConfiguration;
        
        WindowsManager()
        {
            switch (aConfiguration->wmType)
            {
            case MT_GTK:
                aWindowsManager.create<GtkWindowsManager>();
                break;
                
            default:
                throw std::runtime_error("Unknown manager type");
            }
        }
    };
    BIND_TO_SELF_SINGLE(WindowsManager)
    
    // реализация создания IWindow
    PROTO_IFACE(IWindow, a)
    {
        An<WindowsManager> wm;
        wm->aWindowsManager->produceWindow(a);
    }
    
    // реализация создания IButton
    PROTO_IFACE(IButton, a)
    {
        An<WindowsManager> wm;
        wm->aWindowsManager->produceButton(a);
    }
    
    // использование
    An<Configuration> conf;
    conf->wmType = MT_GTK;    // будем использовать gtk
    
    An<IButton> b;
    std::cout << b->getButtonName() << std::endl;
    An<IWindow> w;
    std::cout << w->getWindowName() << std::endl;
    
    // output
    GtkButton
    GtkWindow
    


    Шаблон «прототип»


    Данный шаблон позволяет создавать сложные или “тяжелые” объекты путем клонирования уже существующего объекта. Часто этот шаблон используется совместо с шаблоном синглтон, которых хранит клонируемый объект. Рассмотрим пример:

    // header file
    struct ComplexObject
    {
        std::string name;
    };
    // декларация заливки реализации для класса ComplexObject
    DECLARE_IMPL(ComplexObject)
    
    // cpp file
    struct ProtoComplexObject : ComplexObject
    {
        ProtoComplexObject()
        {
            name = "ComplexObject from prototype";
        }
    };
    // связывание создания ComplexObject с ProtoComplexObject используя прототип
    BIND_TO_PROTOTYPE(ComplexObject, ProtoComplexObject)
    

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

    #define BIND_TO_PROTOTYPE(D_iface, D_prototype)    \
        PROTO_IFACE(D_iface, a) { a.copy(anSingle<D_prototype>()); }
    

    Теперь можно использовать прототип следующим образом:

    An<ComplexObject> o;
    std::cout << o->name << std::endl;
    
    // output
    ComplexObject from prototype
    


    Шаблон «мультитон»


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

    // header
    // описание интерфейса соединения
    struct IConnection : IObject
    {
        virtual void send(const Buffer& buf) = 0;
        virtual Buffer recieve(size_t bytes) = 0;
    };
    // декларация заливки реализации
    DECLARE_IMPL(IConnection)
    
    // cpp file
    // реализация соединения до датацентра
    struct DataCenterConnection : IConnection
    {
        DataCenterConnection()
        {
            std::cout << "Creating new connection" << std::endl;
            // ...
        }
        
        ~DataCenterConnection()
        {
            std::cout << "Destroying connection" << std::endl;
            // ...
        }
        
        // реализация recieve & send
        // ...
    };
    
    // менеджер, который управляет всеми соединениями до датацентров
    struct ConnectionManager
    {
        ConnectionManager() : connectionCount(0), connections(connectionLimit)
        {
        }
        
        void fillConnection(An<IConnection>& connection)
        {
            std::cout << "Filling connection: " << connectionCount + 1 << std::endl;
            if (connectionCount < connectionLimit)
            {
                // создаем новое соединение
                connections[connectionCount].create<DataCenterConnection>();
            }
            // используем уже созданные соединения
            connection = connections[connectionCount ++ % connectionLimit];
        }
        
    private:
        // максимальное количество соединений
        static const size_t connectionLimit = 2;
        
        // текущее количество запрошенных соединений
        size_t connectionCount;
        std::vector<An<IConnection>> connections;
    };
    // связываем менеджер с единственным экземпляром
    BIND_TO_SELF_SINGLE(ConnectionManager)
    
    // реализация создания IConnection
    PROTO_IFACE(IConnection, connection)
    {
        An<ConnectionManager> manager;
        manager->fillConnection(connection);
    }
    
    // использование
    for (int i = 0; i < 5; ++ i)
    {
        An<IConnection> connection;
        connection->send(...);
    }
    
    // output
    Filling connection: 1
    Creating new connection
    Filling connection: 2
    Creating new connection
    Filling connection: 3
    Filling connection: 4
    Filling connection: 5
    Destroying connection
    Destroying connection
    

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

    Синглтон, фабрика и прототип


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

    Итак, приступим. Для начала создадим интерфейсы и объекты, которые хотелось бы создавать:

    struct IShape : IObject
    {
        virtual std::string getShapeName() = 0;
        virtual int getLeftBoundary() = 0;
    };
    
    struct Square : IShape
    {
        Square()                            { std::cout << "Square ctor" << std::endl; }
        Square(const Square& s)             { std::cout << "Square copy ctor" << std::endl; }
    
        virtual std::string getShapeName()  { return "Square"; }
        virtual int getLeftBoundary()       { return m_x; }
        
    private:
        // upper left vertex
        int m_x;
        int m_y;
        // size of square
        int m_size;
    };
    
    struct Circle : IShape
    {
        Circle()                            { std::cout << "Circle ctor" << std::endl; }
        Circle(const Circle& s)             { std::cout << "Circle copy ctor" << std::endl; }
    
        virtual std::string getShapeName()  { return "Circle"; }
        virtual int getLeftBoundary()       { return m_x - m_radius; }
        
    private:
        // center of the circle
        int m_x;
        int m_y;
        // its radius
        int m_radius;
    };
    

    Я дополнил классы неким функционалом, который нам не потребуется, чтобы все выглядело «по-взрослому». Для быстрого поиска мы будем использовать unordered_map, который можно найти либо в boost, либо в std, если ваш компилятор поддерживает новый стандарт. Ключом будет являться строка, обозначающая тип, а значением — объект, который порождает необходимый экземпляр заданного типа. Для этого создадим соответствующие интерфейсы:

    // интерфейс создания объекта нужного типа
    template<typename T>
    struct ICreator : IObject
    {
        virtual void create(An<T>& a) = 0;
    };
    
    // реализация, создающая тип T_impl в качестве реализации интерфейса T
    template<typename T, typename T_impl>
    struct AnCreator : ICreator<T>
    {
        virtual void create(An<T>& a)        { a.create<T_impl>(); }
    };
    
    // реализация, создающая тип T_impl в качестве реализации интерфейса T,
    // использующая шаблон прототип, доставаемый из синглтона
    template<typename T, typename T_impl>
    struct AnCloner : ICreator<T>
    {
        virtual void create(An<T>& a)        { a.copy(anSingle<T_impl>()); }
    };
    

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

    struct ShapeFactory
    {
        ShapeFactory()
        {
            std::cout << "ShareFactory ctor" << std::endl;
            // заполнение контейнера для быстрого поиска ICreator и создания нужного типа
            add<Square>("Square");
            add<Circle>("Circle");
        }
        
        template<typename T>
        void add(const std::string& type)
        {
            // AnCloner создает объекты посредством использования прототипа
            // AnAutoCreate автоматически заполняет нужную реализацию в An<ICreator<...>>
            m_creator.insert(std::make_pair(type, AnAutoCreate<AnCloner<IShape, T>>()));
        }
    
        void produce(An<IShape>& a, const std::string& type)
        {
            auto it = m_creator.find(type);
            if (it == m_creator.end())
                throw std::runtime_error("Cannot clone the object for unknown type");
            it->second->create(a);
        }
        
    private:
        std::unordered_map<std::string, An<ICreator<IShape>>> m_creator;
    };
    // связываем фабрику с синглтоном для "ленивости"
    BIND_TO_SELF_SINGLE(ShapeFactory)
    

    Итак, фабрика готова. Теперь переведем дух и добавим последнюю функцию для порождения объектов:

    void anProduce(An<IShape>& a, const std::string& type)
    {
        An<ShapeFactory> factory;
        factory->produce(a, type);
    }
    

    Теперь фабрику можно использовать:

    std::cout << "Begin" << std::endl;
    An<IShape> shape;
    shape.produce("Square");
    std::cout << "Name: " << shape->getShapeName() << std::endl;
    shape.produce("Circle");
    std::cout << "Name: " << shape->getShapeName() << std::endl;
    shape.produce("Square");
    std::cout << "Name: " << shape->getShapeName() << std::endl;
    shape.produce("Parallelogram");
    std::cout << "Name: " << shape->getShapeName() << std::endl;
    

    Что даст вывод на экран:

    Begin
    ShareFactory ctor
    Square ctor
    Square copy ctor
    Name: Square
    Circle ctor
    Circle copy ctor
    Name: Circle
    Square copy ctor
    Name: Square
    Cannot clone the object for unknown type
    

    Рассмотрим подробнее, что у нас происходит. В самом начале выводится Begin, что означает, что никаких объектов еще не создано, включая фабрику и наши прототипы, говорящее о “ленивости” происходящего. Далее вызов shape.produce(«Square») порождает целую цепочку действий: создается фабрика (ShareFactory ctor), затем рождается прототип Square (Square ctor), затем прототип копируется (Square copy ctor) и возвращается нужный объект. На нем зовется метод getShapeName(), возвращающий строку Square (Name: Square). Аналогичный процесс происходит и с объектом Circle, только теперь фабрика уже создана и повторного создания и инициализации теперь не требуется. При последующем создании Square посредством shape.produce(«Square») теперь вызывается только копирование прототипа, т.к. сам прототип уже создан (Square copy ctor). При попытке создания неизвестной фигуры shape.produce(«Parallelogram») вызывается исключение, которое перехватывается в обработчике, опущенном для краткости (Cannot clone the object for unknown type).

    Выводы


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

    Таблица сравнения: безусловное создание экземпляров
    Шаблон Обычное использование Использование в статье
    Синглтон
    T::getInstance()
    An<T> ->
    Мост
    T::createInstance()
    An<T> ->
    Фабрика
    T::getInstance().create()
    An<T> ->
    Мультитон
    T::getInstance(instanceId)
    An<T> ->

    Таблица сравнения: создание экземпляров на основе входных данных
    Шаблон Обычное использование Использование в статье
    Фабрика
    T::getInstance().create(...)
    An<T>.produce(...)
    Абстрактная фабрика
    U::getManager().createT(...)
    An<T>.produce(...)
    Прототип
    T::getInstance().clone()
    An<T>.produce(...)
    Синглтон, прототип и фабрика
    T::getInstance().getPrototype(...).clone()
    An<T>.produce(...)

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

    Что дальше?


    А дальше — список литературы. Ну а в следующей статье будут рассмотрены вопросы многопоточности и другие интересные и необычные «плюшки».

    Литература


    [1] Хабрахабр: Использование паттерна синглтон
    [2] Хабрахабр: Синглтон и время жизни объекта
    [3] Википедия: Фабричный метод
    [4] Википедия: Порождающие шаблоны проектирования
    [5] Andrey on .NET: Порождающие шаблоны
    [6] Andrey on .NET: Фабричный метод
    [7] Wikipedia: Dependency inversion principle
    [8] Википедия: Обращение контроля
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 42
    • –2
      Мне кажется, что Бритва Оккама не одобряет такой подход.
      • 0
        Можно немного раскрыть комментарий? Что в предлагаемом подходе является излишним?
        • +1
          А мне вот кажется, что Бритва Оккама саму себя не одобряет. Статья расказывает, как введением нескольких новых сущностей сэкономить кучу времени и нервов. Так что они вовсе не являются лишними.
        • –1
          А киньте, пожалуйста, в меня ссылочкой, где кто-нибудь из классиков рассказывает про паттерн «Фабрика»? А то я такой не знаю — знаю только абстрактную фабрику и фабричный метод — и считаю, что фабрику вы только что придумали.
          • +3
            Судя по всему, статью вы не читали:

            Теперь поговорим о наиболее распространенном шаблоне: фабричный метод или просто фабрика.
            • 0
              Да нет, я статью читал.
              Дело в том, что я нахожу это сокращение усложняющим понимание, потому что происходит путаница — фабрика созвучна с абстрактной фабрикой, но обозначает другое понятие, фабричный метод. Поэтому и спросил, а кто из классиков вводит аналогичное сокращение.
              • +1
                В разговорах с коллегами мы обычно употребляем слово «фабрика». В приведенных ссылках тоже используется это слово (то, что нагуглил за пару секунд):
                Фабрика 1
                Фабрика 2
                Строго говоря, лучше, конечно же, использовать понятие «фабричный метод», но сути это никак не меняет.
          • +3
            реализация производящих/создающих функций/методов способом, который Вы использовали (конструкция switch/case внутри порождающей функции) не самый хороший вариант, так как при добавлении нового типа придется исправлять уже готовый и протестированный код, возможно еще и чужой, что так же приведет к необходимости перекомпиляции исправленного и зависимого от него кода… в данном случае гораздо выгоднее использовать внешнюю регистрацию новых типов, которая избавит от всех перечисленных минусов…

            p.s. зачем эта вереница макросов в с++ коде? ностальгия по MFC и ATL? :)
            • 0
              1. В подзаголовке «Синглтон, фабрика и прототип» приведен способ, который позволяет избежать switch/case. В принципе, можно было бы описать подход, который бы избегал полностью решал эту проблему, но это выходит за рамки этой статьи. Здесь конкретная реализация может быть любой, начиная от простой на свичах, и заканчивая более сложной, главное — единый интерфейс.
              2. Про макросы уже обсуждали в предыдущей статье.
              • 0
                1. Мне кажется такой подход как раз входит в рамки данной темы, учитывая, что его реализация крайне проста, а вот дополнительные обертки над shared_ptr, на мой взгляд несколько избыточные, усложняют чтение и понимание приведенного здесь кода и к теме топика отношения как раз имеют мало… так же стоит добавить, что при использовании порождающих паттернов, хорошим тоном является явный запрет создания объектов типов, которые создает фабрика, я вроде у Вас этого не заметил, хотя возможно был невнимателен… интерфейсы это хорошо, но если мы говорим о них в контексте с++, то всегда стоит помнить о зависимостях в коде, которые влияют на работу кода клиента и время компиляции (привет идиома pimpl), что в большом проекте не последний пункт…
                2. Ок, почитаем… :)
                • 0
                  Не совсем согласен. Используя чистый shared_ptr невозможно использовать принцип обращения зависимостей. Это усложнение обоснованное, а не просто ради прихоти. Если вы знаете, как можно реализовать похожий функционал проще, то приведите соответствующий пример. Думаю, всем это будет полезно.

                  По поводу запрета. Если внимательно посмотрите на приведенный код, то все классы-реализации находятся в cpp файле и соответственно недоступны другому коду.

                  Еще раз повторюсь, что здесь я хотел показать принцип обращения зависимостей на примере порождающих шаблонов проектирования.
                  • 0
                    что Вы понимаете под «принцип обращения зависимостей»? можете в 2х словах написать? к сожалению из текста статьи это не до конца ясно…

                    по поводу запрета, понятно… хотя из фрагмента кода с определением интерфейса IShape и его дочерних классов, это не совсем очевидно, но можно догадаться… :)
            • +1
              На тему возвращения указателей из функций, мне этот момент тоже не нравится. По-хорошему, нужно вместо указателей возвращать обёртки определяющие владение/удаление, и никогда не возвращать голые указатели. В качестве обёртки похоже идеальный вариант — unique_ptr с подходящим deleter'ом, но можно и без C++0x выкрутиться. Правда, подозреваю, что на практике никто так делать не будет и это вообще мало кого волнует…
              • 0
                Да, как вариант можно использовать unique_ptr. Однако с ним связана определенная сложность: он обладает move-семантикой и это может приводить к неприятным последствиям. С ним надо проявлять осторожность, в то время как shared_ptr можно без каких-либо проблем копировать. Однако с синглтонами такой фокус не пройдет (имеется в виду использование unique_ptr), т.к. надо шарить данные между различными клиентами.
                • 0
                  Да вроде unique_ptr хоть и с move-семантикой, не создаёт таких граблей как auto_ptr — если уж скомпилировался, то будет работать. Хотя ограничений, конечно, добавит. Но имхо даже не суть важно, unique_ptr или какая другая обёртка, важно чтобы владелец был чётко определён.

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

                  В случае синглтона владение (ownership) обычно не передаётся, а значит и shared_ptr не обязателен. shared_ptr — это не «шарить данные», а шарить владение. Не вижу, что мешает в этом случае для возвращения из функции использовать unique_ptr, например с пустым deleter'ом.
                  • +1
                    Хотя ваша точка зрения про синглтоны понятна, у вас свой синглтон, со всем что нужно :) и с неопределённым владельцем. Но здесь я скорее поддерживаю критиков из предыдущей статьи — уж больно сложно (+оверхед), а польза неочевидна.
                    • 0
                      А в чем сложность? Если посмотреть на другие вещи, например реализацию того же shared_ptr или просто std::vector, то там сложность на порядки выше. An — это всего лишь легкая обертка вокруг shared_ptr с вполне понятной семантикой.
                      • 0
                        Так реализацию shared_ptr не заставляют изучать, просто берёшь и пользуешься. У вас же другой случай, вы не про общепринятый boost рассказываете, и поэтому нужно разбираться со всеми биндингами и прочей шаблонной магией. Но зачем, если людям непонятно в чём бонус?
                        • 0
                          Как минимум, при обращении к данным shared_ptr упадет в случае отсутствия таковых, в то время как мой подход кидает исключение, что является более безопасным способом. Уже это серьезно отличает от shared_ptr и является очень полезным. Я не говорю о других достоинствах, которые значительно упрощают жизнь. Поэтому использование дополнительного класса более, чем оправдано.
                          • 0
                            Ну, во-первых, бросание исключения — вопрос спорный (безопасность против производительности), и поэтому лучше это оставлять параметром как упоминалось в комментах к предыдущему топику. Во-вторых, если бы речь шла только об этом, то это ооочень тонкая обёртка, куда проще чем то, что вы предлагаете. И наконец, я говорю о неочевидности для людей преимущества вашего подхода над «обычными» синглтонами, в которых и shared_ptr-то не нужны, не то что его «улучшения».

                            В общем, мне кажется, что мотивация для всего этого недостаточно раскрыта, особенно если речь не только о синглтонах.
                            • 0
                              прошу прощения за некоторый оффтоп… но как исключения влияют на производительность?
                              • 0
                                Для того, чтобы бросить исключение, надо эту ситуацию обнаружить, т.е. вставить соответствующую проверку. Такая проверка будет выполняться даже в том случае, если все хорошо. Именно поэтому будет замедление. Но мне кажется, что это есть преждевременная оптимизация. Для меня лучше иметь стабильное поведение, чем непонятные креши. А сделать профайлинг и заоптимизировать можно в любой момент, отказавшись от этого if.
                                • 0
                                  Кроме того сама возможность того, что более глубокий код бросит исключение, приводит к дополнительным проверкам и запрете оптимизаций, т.к. понадобится раскрутка стека.

                                  • 0
                                    Вообще говоря, это не так. Смотрите, например: комментарий на stackoverflow, а также секцию 5.4 в Technical Report on C++ Performance
                                    • 0
                                      Ну в частности в упомянутом комментарии на SO говорится, что на 32-битном msvc как раз будет оверхед. Кроме того, если даже в случае если нет очевидного оверхеда, оптимизации наверняка идут лесом, а они могут в разы улучшать производительность.

                                      Вообще, имхо бессмысленно спорить на эту тему, всё уже «украдено до нас», дебаты на эту тему так и продолжаются, а значит и серебряной пули нет.
                                    • 0
                                      Какого рода дополнительные проверки не считая try/catch, который в минимальном варианте может распологаться на самом верху? Какого рода запреты оптимизаций, можно пример?
                                      • 0
                                        Дополнительные проверки, добавляемые компилятором, о том что было брошено исключение. Эти проверки, очевидно, будет располагаться не только на самом верху, но и вообще по всему стеку вплоть до самого низа, т.к. исполнение может быть прервано в любой момент. Насчёт оптимизаций — изначально предлагалось к функциям добавлять спецификации вроде nothrow, которые позволяют компилятору лучше оптимизировать (что конкретно — я не в теме). Правда, насколько я знаю, такого рода спецификации ныне deprecated.
                                      • 0
                                        относительно раскрутки стека, она понадобится только тогда, когда будет сгенерировано исключение, которое необходимо генерировать в исключительных ситуациях, при возникновении которых, ни о какой производительности уже речь не идет, так как надо обрабатывать такую ситуацию…
                                        • 0
                                          Сама раскрутка стека — да, но сама возможность того, что она понадобится, очевидно, добавляет ограничения.
                                      • 0
                                        Вообще, вопрос использовать исключения или нет далеко неочевидный, т.к. при их использовании нужно гарантировать exception-safety всего кода. Гугл в частности поэтому от них отказался, да и вообще дебаты на эту тему не утихают. Поэтому я и говорю, что вопрос спорный. Я тоже предпочитаю исключения и стабильное поведение, но в реальности есть аргументы и в ту и в другую сторону, поэтому нельзя решать за всех (наверное, это одна из причин, почему shared_ptr не бросает исключений, не просто так же они «не догадались»).
                                        • 0
                                          Если писать как попало, то исключения использовать вредно. А если использовать единый подход, например, как предложенный в статье, то проблем не будет. Я использую исключения в своих проектах и, поверьте, все работало как часы.
                                          • 0
                                            Это просто требует другого стиля программирования, в частности требует практически повсеместного использования RAII. Я не спорю, что можно всё написать правильно, я лишь говорю, что это сделать сложно, и поэтому вопрос неочевидный, а вы мне отвечаете «проблем не будет» и «поверьте» :)
                                            • 0
                                              Вы скорее всего опять мне не поверите, но сделать это гораздо проще, чем Вы пытаетесь представить… это просто требует стиля программирования в стиле с++ :)
                                              • 0
                                                Ну да, а обсуждения на тему exception safe code по всему интернету это тоже выдумки и профанация :)
                                                «Гораздо проще» чем «сложно» — это как? ;)
                                                • +1
                                                  у меня иногда создается впечатление, что так и есть… это я про обсуждения на тему exception safe code по всему интернету, ни одного вменяемого довода в пользу не использования исключений я не слышал, весь этот оверхед, который обычно сразу начинают вспоминать просто смешной, он настолько мал, что им можно без всяких сомнений пренебречь…

                                                  ну вот вспоминая проекты на с++ в которых я принимал участие, я не помню каких-либо проблем при использовании в качестве обработки ошибок исключений… а было в этих проетах всего полно, и легаси код без исключений на ретвалах, и старые либы а ля zlib, и COM-компоненты, как свои, так и чужие и различные библиотеки а ля boost/loki/atl… и никогда из-за этого никаких проблем не было, все аккуратно оборачивалось соответствующими обертками, которые использовали общий для проекта фрэйворк для обработки ошибок, построенный на исключениях и использовалось в основном коде… естественно для того чтобы все корректно работало, код должен писаться в стиле с++, а не в стиле с with classes как многие любят… :)
                                                  • 0
                                                    Для меня «в стиле с++» звучит как использование «симметричных» RAII-объектов по-максимуму. В этом случае действительно работа с исключениями упрощается, но только до тех пор, пока не окажется, что исключение может быть выброшено в деструкторе… Например, если функция закрытия файла может выбросить исключение, а мы хотим завернуть открытие-закрытие в RAII-обёртку, то выйдет облом :) Кстати, было как-то обсуждение похожей ситуации в java, там нет RAII но аналогичным образом исключения в finally бросать нехорошо, а закрытие файла — бросает.

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

                                                    Вообще, получается любая работа с внешним объектом должна быть транзакционной и обязательно заканчиваться коммитом, что далеко не всегда получится автоматически, даже в стиле с++. А вы как с этим справлялись?
                                                    • 0
                                                      «в стиле с++» это не только RAII, хотя конечно эта концепция одна из самых востребованных в контексте обработки ошибок при помощи исключений, да и не только… все описывать в рамках этой темы смысла нет, есть достаточное количество литературы по этому вопросу…

                                                      про исключение в деструкторе, это пример по-большому счету высосанный из пальца, опять же в здравом уме это никто делать не будет, на практике при работе с различными библиотеками я такого ни разу не встречал… java не c++ поэтому тут обсуждать нечего…

                                                      приведите пример того, что нельзя обернуть, пример с Builder я не догнал, так же как и с обязательной транзакционностью…
                                        • 0
                                          то есть Вы предлагаете забить на обнаружение таких ситуаций? Тогда о каком стабильном поведении может идти речь и откуда берутся непонятные креши?
                      • –2
                        В целом — мне непонятно, в чем ваша статья отличается от других аналогичных статей, которых тьма на том же codeproject. Про фабрики и С++ — есть книжка Александреску, на которую вы не сослались. Там и про полиморфные и про абстрактные фабрики. Про порождающие паттерны в целом лучше всего написано в книгде Refactoring to patterns. Ее я то же не вижу в списке литературы.
                        Примеры ваши — toy samples. Они не показательны. Я понимаю, что доступные real life примеры сложно придумать — но в том-то и сложность написания статей по тематике.
                        Есть еще менее известный паттерн Trader [1], о котором на русском нет статей.
                        Ну и имхо — лучше с позиции рефакторинга паттерны рассматривать. Мне хочется верить, что это не преумножает pattern-happy программистов.
                        [1] Product Trader, by Baumer & Riehle in Pattern Languages of Program Design 3, Eds Martin, Riehle, Buschman, 1998, pp 29-46.
                        • 0
                          В статье ясно сказано, что Данная статья не претендует на полноту изложения таких шаблонов. Моя цель была несколько в другом, о чем я в начале и в конце не раз явно упоминал. Более того, как раз приведенная Вами ссылка [1] содержит все те проблемы, о которых я рассказывал и пытался избежать. И если вы внимательно почитаете выводы, то я рассматривал шаблоны именно с позиции рефакторинга.
                          • 0
                            Я честное слово не вижу рассказа с позиции рефакторинга. Т.е. я под этим подразумеваю a-la «вот был такой-то код, он решал такую-то задачу. Теперь мы захотели его исправить применив связку таких-то паттернов.»
                            Про статью я невнятно выразился. Пример статьи я привел не для того чтобы Вы писали про этот конкретный паттерн. А к тому, что есть паттерны которые не так широко афишированы как GoF'ские, но могут быть интересны широкой аудитории. А о паттернах GoF написано настолько много статей, что сложно добиться новизны при написании собственной статьи. Кроме, быть может, подбора интересных примеров использования. Этого я как раз не вижу — только toy samples.
                            Резюмируя критику — недостаточный список литературы, отсутствие новизны как по содержанию, так и по способу подачи материала.
                            Хотя это не значит, что я считаю вашу статью не полезной — у нас в сообществе до сих пор паттерны это что-то сложное и космическое. Образовывать массы надо. Но массы пишут больше про особенности реализации, чем про ООД, что и печалит и наводит на мысли.
                        • +1
                          Бррр… с++ и слово «шаблоны», когда уже общепринятый термин «паттерны проектирования» чтобы не путать template из языка.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.