Yet another factory

в 7:54, , рубрики: c++, factory, Qt Software, ооп, метки:

В текущем проекте стала часто возникать необходимость конструирования множеств разнообразных объектов по каким-то идентификаторам. Была написана одна фабрика для какого-то множества, другая. Потом пришло понимание, что мы делаем одно и то же и нужно какое-то повторяемое решение.
Проект базируется на 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;
};

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

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

Макросы
#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 T> 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: возможно я снова изобрел лисапед?

Автор: ncix

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js