Pull to refresh

Yet another factory

Reading time 3 min
Views 2.9K
В текущем проекте стала часто возникать необходимость конструирования множеств разнообразных объектов по каким-то идентификаторам. Была написана одна фабрика для какого-то множества, другая. Потом пришло понимание, что мы делаем одно и то же и нужно какое-то повторяемое решение.
Проект базируется на 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: возможно я снова изобрел лисапед?
Tags:
Hubs:
+17
Comments 27
Comments Comments 27

Articles