Аргументированная фабрика

в 17:06, , рубрики: c++, Qt Software, фабрика объектов, шаблоны, метки: , ,

Доброго времени суток!

Задача

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

Могу предложить два способа решения данной задачи.

Исходные данные

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

class IFactoryBasic
{
public:
    IFactoryBasic() {}
    virtual ~IFactoryBasic() {}
    virtual Test* create(const QString &key, const QString &args)=0;
};

где Test — некий базовый класс

class Test
{
protected:
    QString _word;
public:
    Test(const QString &word):_word(word) {}
    virtual ~Test() {}
    virtual void test(){ qDebug()<<"test "<<_word; }
};

class TestChild: public Test
{
public:
    TestChild(const QString &word): Test(word) {}
    virtual void test() { qDebug()<<"test child"<<_word; }
};

TestChild — наследник Test
Оба класса принимают в конструкторе строковый параметр word, который потом мы можем верифицировать в функции test().

Первый способ

Способ простой. Он основан на создании шаблонного каркаса для будущей фабрики.

template<class T, class C, class A>
class IFactory
{
    QMap<QString, C* (T::*)(const A&) > handler;
protected:
    void add(const QString &key, C *(T::*func)(const A &))
    {
        handler[key]=func;
    }

public:
    IFactory() {}
    virtual ~IFactory() {}
    C *make(const QString &key, const A &args)
    {
        if(handler.contains(key))
        {
            T* inheritor = dynamic_cast<T*>(this);
            if(inheritor)
                return (inheritor->*handler[key])(args);
        }
        return 0;
    }
};

Здесь есть маленькое обязательство для пользователей класса. Первый шаблонный параметр должен быть классом, который наследуется от IFactory. Далее будут пояснения, для чего это было нужно.
handler в классе IFactory — ассоциативный контейнер, содержащий ключ и соответствующую функцию создания объекта. Сигнатура функции порождения описывается как C* (T::*)(const A&), то есть возвращаемое значение будет иметь указатель на некий класс C, как аргумент функции передается ссылка на объект типа A.
Функция add(...) добавляет в контейнер пару ключ-функция <key,func>.
Функция make(...) вызывает функцию порождения, если она имеется в контейнере (предварительно динамически преобразовав тип указателя this к типу наследника, иначе нельзя вызвать функции, которые там были определены).
Это основной каркас фабрики, осталось описать конкретную фабрику

class FactoryFst: public IFactory<FactoryFst, Test, QString>, public IFactoryBasic
{
    Test *createOrigin(const QString &args){ return new Test(args); }
    Test *createChild(const QString &args) { return new TestChild(args); }
public:
    FactoryFst()
    {
        add("test", &FactoryFst::createOrigin);
        add("testchild", &FactoryFst::createChild);
    }

    Test *create(const QString &key, const QString &args) { return make(key, args); }
};

Нетрудно догадаться, что мы используем множественное наследование для удовлетворения требованиям интерфейса IFactoryBasic. Для другого родителя мы явно указываем наследника FactoryFst, возвращаемый указатель будет указателем на объект класса Test, и в качестве аргумента передается ссылка на объект QString.
В соответствии с этим определением создаются функции, генерирующие объекты типа Test и TestChild:
Test *createOrigin(const QString &args){ return new Test(args); } — создает объект типа Test, передавая ему в конструктор аргумент QString.
Test *createChild(const QString &args) { return new TestChild(args); } — аналогично создает объект типа TestChild.
Остается только в конструкторе FactoryFst зарегистрировать данные функции и определить функцию create(...) интерфейса IFactoryBasic.

Второй способ

Этот метод в большей степени использует шаблоны.
Для построения фабрики нам нужна небольшая подготовка. Для начала нужно определить некоторые используемые фабрикой классы.

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

class Arguments
{
public:
    virtual ~Arguments() {}
};

template<class T>
class TArguments: public Arguments
{
public:
    T arg;
    TArguments(T _arg):arg(_arg) {}
};

Arguments — базовый класс, TArguments — шаблонный класс для хранения передаваемого объекта.

Так же нам понадобится класс-шаблонная обертка для вызова оператора new.

class Container
{
public:
    virtual ~Container() {}
    virtual void *make( Arguments* ) = 0;
};

нешаблонный Container служит той же цели, что и Arguments. Чтоб мы всегда могли вызвать функцию порождения make(...) для любого его шаблонного наследника. Функция make(...) должна возвращать указатель на созданный объект.


template<class T, typename A>
class TContainer: public Container
{
public:
    void *make(Arguments* args=0)
    {
        TArguments<A>* a = dynamic_cast< TArguments<A>* >( args );
        if(!a) return 0;
        return new T(a->arg);
    }
};

класс TContainer уже шаблонный, в качестве аргументов шаблона ему передается тип возвращаемого указателя T и тип аргумента для конструктора A.
В функции make(...) в качестве аргумента передается указатель на Arguments, но мы-то понимаем, что на самом деле это должен быть указатель на TArguments и пробуем динамически преобразовать тип. Если все преобразования прошли успешно, то можем создавать объект ранее определенного типа.

В итоге, каркас фабрики будет выглядеть следующим образом:

template<typename C=void, typename A=void>
class TemplateFactory
{
    QMap<QString, Container*> handler;

public:
    TemplateFactory() {}
    virtual ~TemplateFactory(){}

    template<class T>
    void add(const QString &name)
    {
        handler.insert(name, new TContainer<T, A>());
    }

    C *make(const QString &name, const A &arg)
    {
        if(handler.contains(name))
            return static_cast<C*>(handler.value(name)->make(new TArguments<A>(arg)));
        return 0;
    }
};

Здесь аргументы шаблона C — базовый класс, A — аргумент конструктора порождаемых объектов.
Функция add(...) регистрирует новые классы в списке, make(...) создает объект класса, передавая в функцию ключ для выбора типа и аргумент конструктора. Преобразование типов static_cast используется для преобразования типа void* в нужный нам C*.
Все готово, чтоб создать конкретную фабрику.


class FactorySnd: public TemplateFactory<Test, QString>, public IFactoryBasic
{
public:
    FactorySnd()
    {
        add<Test>("test");
        add<TestCild>("testchild");
    }

    Test* create(const QString &name, const QString &arg){ return make(name, arg); }
};

Опять используется множественное наследование, переопределяется функция create(...). В конструкторе происходит регистрация классов.

Результат

Обе фабрики работают в чем можно удостовериться, выполнив следующий код

    IFactoryBasic* factory  = new FactorySnd();
    Test* test = factory->create("test", "A");
    test->test();
    test = factory->create("testchild", "B");
    test->test();
    delete factory;
    factory = new FactoryFst();
    Test *stest = factory->create("test", "C");
    stest->test();
    stest = factory->create("testchild", "D");
    stest->test();

В консоли получаем следующий выхлоп:

test  "A" 
test child "B" 
test  "C" 
test child "D"
Заключение

Одна и та же задача, конечно же, может иметь не одно решение. Представленные решения не являются единственными и непогрешимыми. Первый способ дает большую свободу, поскольку предоставляет право определять методы генерации объектов, ведь в этом же методе можно связать этот объект сигналами (если он наследник QObject) или зарегистрировать его в Наблюдателе. Можно так же как-то видоизменить передаваемый в порождающий метод аргумент перед передачей его в конструктор. Но плата за это — более сложное сопровождение кода, добавление нового порождаемого объекта. Второй метод менее требователен в этом отношении, но оставляет меньше свободы пользователю. То есть создание объекта происходит так, как это описано в базовом классе и никак иначе. Так же фабрики ориентированы на создание объектов наследуемых от одного класса и принимающих в качестве аргумента один объект. В этом объекте-инициализаторе должны быть инкапсулированы все необходимые свойства для создания нового объекта. Это, конечно, не универсально, (накладывает определенные ограничения на создаваемые объекты) но для решения прикладной задачи вполне подходит.

Автор: dandemidow

Источник


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


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