Yet another factory

    В текущем проекте стала часто возникать необходимость конструирования множеств разнообразных объектов по каким-то идентификаторам. Была написана одна фабрика для какого-то множества, другая. Потом пришло понимание, что мы делаем одно и то же и нужно какое-то повторяемое решение.
    Проект базируется на Qt, который, как известно, имеет развитые механизмы работы с метаданными. Тем не менее конструирование объектов через QMetaObject нас не удовлетворяло по двум причинам: во-первых конструируемые объекты должны быть QObject'ами, а во-вторых при конструировании мы получаем указатель на QObject, который так или иначе придется преобразовывать, что чисто эстетически некрасиво.

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

    BaseClass * instance = BaseClass::factory()->build("derived_name");


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

    Немного поразмыслив, придумали фабрику, решающую широкий круг наших задач.

    Фабрика

    Для начала приведу код фабрики, благо он получился компактным:
    template<class Base>
    class UnifiedFactory
    {
    public:
        UnifiedFactory(){}
        ~UnifiedFactory(){qDeleteAll(m_builders);}
        template<class T>
        void registerClass(const QString& name)
        {
            delete m_builders.value(name);
            m_builders.insert(name, new Builder<T>());
        }
        Base * build(const QString& name) const
        {
            BaseBuilder * builder = m_builders.value(name);
            if(builder)
                return builder->build();
            return 0;
        }
    private:
        class BaseBuilder
        {
        public:
            virtual Base * build() const = 0;
        };
        template<class T>
        class Builder : public BaseBuilder
        {
        public:
            virtual Base * build() const { return new T(); }
        };
        typedef QHash<QString, BaseBuilder*> Builders;
        Builders m_builders;
    };
    


    Как видно, данная шаблонная фабрика умеет конструировать только объекты с общим базовым классом .
    Также, она имеет шаблонный метод template registerClass(const QString& name), который регистрирует класс наследника T в нашей фабрике под каким-то строковым именем.
    Непосредственным конструированием сама фабрика не занимается, а делегирует эту задачу множеству маленьких классов-билдеров, имеющих общего предка BaseBuilder
    с виртуальным методом build() и шаблонных(!) наследников
    Builder, специализируемых классом регистрируемого наследника. 
    Специализированные билдеры создаются при регистрации и сохраняются в хэш-контейнер фабрики. При необходимости сконструировать нужного наследника по имени мы вытаскиваем из хеша нужного билдера, который и делает всю работу. Если билдера нет, значит класс не зарегистрирован, возвращаем ноль.
    Благодаря смеси статического и динамического полиморфизма получилось всё достаточно просто и элегантно.

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

    Макросы


    #define UNIFIED_FACTORY(BaseType) \ static UnifiedFactory<BaseType>* factory() \ { \ static UnifiedFactory<BaseType> s_factory; \ return &s_factory; \ } \ template<class T> \ class AutoRegistrer \ { \ public: \ AutoRegistrer(const QString& name){factory()->registerClass<T>(name);} \ }; \ #define UF_REGISTER_DERIVED_NAMED(type, name) \ static const type::AutoRegistrer<type> type##Registrator(name); #define UF_REGISTER_DERIVED(type) UF_REGISTER_DERIVED_NAMED(type, #type)


    Первый макрос добавляется в тело базового класса, создавая в нем статический метод со статической же фабрикой и служебным шаблонным классом-регистратором
    template class AutoRegistrer, единственная задача которого - в своем конструкторе зарегистрировать в фабрике класс-аргумент шаблона.

    Макрос UF_REGISTER_DERIVED_NAMED регистрирует класс под заданным именем, размещая в модуле статический объект класса AutoRegistrer.
    Последний макрос делает то же самое, но регистрирует класс под собственным именем

    Примеры

    Проиллюстрирую, что же даёт нам эта фабрика:
    class Base { publiс: UNIFIED_FACTORY(Base) }; class Derived1 : public Base {...} UF_REGISTER_DERIVED(Derived1) class Derived2 : public Base {...} UF_REGISTER_DERIVED(Derived2) .... Base * instance = Base::factory()->build("Derived1")


    Что же получилось


    Я знаю, что серебрянных пуль не бывает, как не бывает и универсальных решений на все случаи жизни. Данная фабрика позволяет решать ограниченный но достаточно широкий круг задач по конструированию разнообразных наследников какого-то базового класса. Фабрику можно использовать как макросами так и сделать синглтоном или членом другого класса, в зависимости от задачи.
    В решении, используются Qt-шные строки и хэш, которые легко заменить STL-ными аналогами.
    Из недостатков стоит отметить дополнительные расходы на память для билдеров и регистраторов, а также некоторый оверхед при поиске билдера по хешу. Внимательный читатель конечно найдет еще, а я с удовольствием приму к сведению.

    PS: возможно я снова изобрел лисапед?
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 27

      +1
      BaseBuilder'у очень не повредил бы виртуальный деструктор, а UnifiedFactory не плохо было бы сделать NonCopyable, всеж в нем есть поля указателей, хоть и в мапе…
        0
        Согласен, спасибо
        +1
        Я тоже искал готовые решения, но не нашел. Поэтому сделал свою фабрику. Я старался сделать ее максимально гибкой, и без использования макросов. По умолчанию все обьекты регистрируется динамически во время выполнения, но никто не мешает сделать статический инициализатор.
          0
          Пардон, у меня теги не работают (вставлял линк в мессадже выше): habrahabr.ru/post/129202/
            0
            Да, ваше решение практически идентичное. Это говорит о том что решение удачное.
              0
              Да уж, лучше фишками С++11 типа auto и decltype юзать, если уж так хочется сократить объем кода на кастах.
              0
              Подобные решения очень часто стоят рядом со стратегиями. Я писал нечто подобное, но плохо дружу с макросами, поэтому получалось не очень красиво… Взял макросы на заметку.
                0
                В кутиме аналогичный прием уже столет в обед юзается, но без этих страшных макросов.
                  0
                  в рантайме регистрация?
                    0
                    Естественно, но там фабрика не универсальная, а от того она значительно более простая и понятная.
                  +2
                  ~UnifiedFactory(){qDeleteAll(m_builders);}
                  

                  Закатали бы лучше в QScopedPointer'ы, заодно бы получили невозможность копировать объект и автоматическое удаление. Ну или в случае если копирование нужно, тогда QSharedPointer
                    –2
                    и накладные расходы на умный указатель. Нет, пусть будет попроще. По-поводу запрета копирования замечание принял.
                      0
                      Накладные расходы в фабрике? Это даже не на спичках экономия!
                        –3
                        Профит сомнительный и читаемость хуже…
                        Ну не люблю я умные указатели юзать без острой необходимости :)
                          0
                          typedef, auto, decltype?
                    +2
                    Что Вы будете делать в случае, если создаваемому объекту понадобится некий параметр при конструировании?
                    Как по мне, то регистрировать стоит не конечный класс, а его билдер, либо уже созданный объект класса с клонированием (прототип).

                    Еще, при необходимости, можно сделать шаблонный метод для конструирования классов конкретного типа (не базового). Это позволит в некоторых случаях избежать нежелательной конвертации к классу-наследнику.
                    Я использовал похожую конструкцию как IoC контейнер, но там у меня хранились объекты разных классов без общего предка (по принципу boost::any).
                      0
                      >>Что Вы будете делать в случае, если создаваемому объекту понадобится некий параметр при конструировании?

                      Не буду использовать данную фабрику :) вообще в таких случаях я вместо аргументов в конструкторе делаю некий init()

                      >>Еще, при необходимости, можно сделать шаблонный метод для конструирования классов конкретного типа (не базового)
                      Если вы в данном контексте знаете конкретный класс, зачем его конструировать через фабрику?
                      Если же этот класс сам является базовым для какого-то подможества — в данной реализации правильнее засунуть фабрику в него.
                        0
                        >>Если вы в данном контексте знаете конкретный класс, зачем его конструировать через фабрику?
                        Клиентский код может иметь доступ к декларации класса, но при этом не уметь его конструировать.

                        Конечно, все зависит от конкретной ситуации, я просто делал нечто подобное и вспомнил об этом.
                        Пример того, что я делал см. ниже.
                        0
                        >> Как по мне, то регистрировать стоит не конечный класс, а его билдер
                        Да, была такая мысль. Такое решение может быть гибче. Можно использовать ту же фабрику но с кастомными билдерами, а можно с шаблонными. В будущем если понадобится — так и сделаю.

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

                          +1
                          Хочу привести пример IOC контейнера, о котором я упоминал:

                          class CContainer
                          {
                              struct SortPred
                              {
                              public:
                                  bool operator ()(const boost::any& one, const boost::any& two)
                                  {
                                      return one.type().before(two.type()) ? true : false;
                                  }
                              };
                              std::set<boost::any, SortPred> m_objects;
                          
                          public:
                              template<typename T>
                              void RegisterObject(T* pT)
                              {
                                  m_objects.insert(boost::any(pT));
                              }
                              template<typename T>
                              T* GetObject()const
                              {
                                  T* pT = NULL;
                                  auto iter = m_objects.find(boost::any(pT));
                                  if (iter == m_objects.end())
                                  {
                                      return NULL;
                                  }
                                  return boost::any_cast<T*>(*iter);
                              }
                              template<typename T>
                              void UnregisterObject(T* pT)
                              {
                                  auto iter = m_objects.find(boost::any(pT));
                                  if (iter != m_objects.end())
                                  {
                                      m_objects.erase(iter);
                                  }
                              }
                          };
                          
                          • UFO just landed and posted this here
                              0
                              1. Где вы вообще увидели MFC, и что за претензии к использованию C++11 в 2012 году?

                              2. std::set в примере, т.к. мне нужно было написать пример быстро и по сути (оригинальный код я показать не могу).
                              В оригинале у меня std::map с хитрым составным ключом, включающим в себя type_info + строчку.
                              Таким образом, что результатом поиска может быть только объект определенного типа (так же, как и в примере с std::set). Единственное отличие при использовании std::map это то, что я могу добавлять в контейнер любое количество объектов разного типа с нулевым ID (как и в примере), и несколько объектов одного типа с разными ID:
                              A* a = container.GetObject<A>();
                              B* b1 = container.GetObject<B>("b1");
                              B* b2 = container.GetObject<B>("b2");
                              

                              В общем, оригинальный пример мне показался перегруженным и я сократил его до std::set.

                              3. type_info::berofre() возвращает int, а не bool.
                              • UFO just landed and posted this here
                                  –1
                                  Так, как ты мне тут тыкаешь, то с тобой мне точно больше говорить не о чем!
                          +1
                          У вас статический объект для регистрации будет инициализирован для каждой единицы трансляции в которую включен заголовочный файл с макросом UF_REGISTER_DERIVED в результате регистрация может быть вызвана несколько раз.

                          чтобы избежать этого можно воспользоваться приемом который называется счетчик Шварца (Jerry Schwarz counter)

                          template<class T>
                          class AutoRegistrer
                            {
                            public:
                              AutoRegistrer(const QString& name)
                              {
                                if (registered_++ == 0)
                                  factory()->registerClass<T>(name);
                              }
                            private:
                              static int registered_;  
                          };
                          
                          template<class T> int AutoRegistrer<T>::registered_= 0;
                          

                            0
                            Совершенно верно. Но в моем решение повторная регистрация просто «затрёт» старую, никаких проблем не будет. Спасибо за интересное решение.

                          Only users with full accounts can post comments. Log in, please.