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