Pull to refresh

Обращение зависимостей и порождающие шаблоны проектирования

Reading time 18 min
Views 13K

Аннотация


Это третья статья, просвещенная порождающим шаблонам проектирования и связанным с ними вопросами. Здесь мы рассмотрим излюбленные приемы при создании объектов: фабрики, заводы, абстрактные фабрики, строители, прототипы, мультитоны, отложенные инициализации, а также немного коснемся pimpl идиомы или шаблона “мост”. Использование синглтонов было подробно рассмотрено в первой [1] и второй [2] статьях, однако, как вы увидите в дальнейшем, синглтоны часто используются совместно с другими шаблонами проектирования.


Введение


Многие наверняка слышали, читали или даже использовали порождающие шаблоны проектирования [4]. В этой статье как раз и будет идти о них речь. Однако здесь акцент будет делаться на других вещах. Конечно же, эту статью можно использовать как справочник по порождающим шаблонам, или как введение в них. Но моя конечная цель находится несколько в иной плоскости, а именно, в плоскости использования этих шаблонов в реальном коде.

Не секрет, что многие, узнав о шаблонах, стараются начать их повсеместно использовать. Однако не все так просто. Многие статьи этой тематики не уделяют должного внимания их использованию в коде. А когда начинают прикручивать шаблоны к коду, тут возникает нечто такое невообразимое, что ни в сказке сказать, ни пером описать. Мне доводилось видеть различные воплощения этих идей, иногда невольно задаешься вопросом: а что курил автор? Взять, к примеру, фабрику или фабричный метод из Википедии [3]. Я не буду приводить весь код, приведу лишь использование:

const size_t count = 2;
// An array of creators
Creator* creators[count] = { new ConcreteCreatorA(), new ConcreteCreatorB() };

// Iterate over creators and create products
for (size_t i = 0; i < count; i++) {
    Product* product = creators[i]->factoryMethod();
    cout << product->getName() << endl;
    delete product;
}

for (size_t i = 0; i < count; i++)
    delete creators[i];

Если себя спросить, а как это использовать в реальной жизни, то сразу возникают следующие замечания:
  1. Как я узнаю, что мне надо использовать именно 0-й или 1-й элемент? Они ничем не отличаются.
  2. Предположим, что надо в цикле создать некие элементы. Откуда я возьму знание о том, где находятся эти фабрики? Если я фабрики инициализирую тут же, то зачем мне они вообще нужны? Можно просто создать объект и позвать определенный метод или отдельно стоящую функцию, которая все сделает.
  3. Предполагается, что объекты создаются оператором new. Тут сразу возникает вопрос с обработкой исключительных ситуаций и времени жизни объекта.

Как ни крути, а данный пример лишь некая иллюстрация, которая содержит множество изъянов. В реальной жизни такое не используют.

«А что же используют тогда?», спросит внимательный читатель. Ниже приведен код использования. Этот список не претендует на полноту:
// создание объекта, используя фабрику, получаемую из синглтона
Object* o = Factory::getInstance().createObject("object_name");

// использование конфигурации для создания объектов
Configuration* conf = Configuration::getCurrentConfiguration();
Object* o = Factory::getInstance().createObject(conf->getObjectNameToCreate());

Стоит обратить внимание, что фабрики в «реальной жизни» как правило являются синглтонами. Также можно заметить, что при создании объектов «торчат уши» использованных шаблонов. При последующем рефакторинге это даст о себе знать с неприятной стороны. Часто используется подход, когда возвращаются объекты по указателю. Так учили во всех книжках, так продолжается писаться код. Если с методом createObject все ясно — надо позвать delete в конце, то что делать с конфигурацией? Это синглтон или нет? Если да, то ничего делать не нужно. А если нет? Возникают опять вопросы с временем жизни. Про правильную обработку исключений тоже не надо забывать, и такой код с обработкой исключительных ситуаций вызывает проблемы, связанные с чисткой ресурсов.

Как ни крути, а хотелось бы иметь единый подход, который бы проходил красной нитью сквозь порожденные объекты и не отличал различные способы создания, коих множество. Для того, чтобы претворить это в жизнь, будем использовать мощный принцип обращения зависимостей [7]. Суть его состоит в том, что вводится некая абстракция, интерфейс. Далее использующий и используемый код связывается посредством введенного интерфейса используя, например, обращение контроля [8]. Это позволяет коду, который хочет создавать объекты, абстрагироваться от конкретики создания класса и просто использовать выделенный интерфейс. Вся забота ложится на плечи функционала, реализующего этот интерфейс. В статье подробно рассмотрены способы создания объектов с использованием практически всех известных порождающих шаблонов проектирования, а также приведен пример, когда для создания экземпляров используется несколько порождающих шаблонов одновременно. Пример синглтона подробно описан в предыдущей статье [2], в этой статье будет лишь его использование совместно в другими шаблонами.

Инфраструктура


Объект An и инфраструктура вокруг него подробно описана во второй статье [2]. Здесь я лишь приведу код, который будет использоваться в дальнейшем повествовании. Для уточнения деталей смотрите предыдущую статью [2].

template<typename T>
struct An
{
    template<typename U>
    friend struct An;

    An()                              {}

    template<typename U>
    An(const An<U>& a) : data(a.data) {}

    template<typename U>
    An(An<U>&& a) : data(std::move(a.data)) {}

    T* operator->()                   { return get0(); }
    const T* operator->() const       { return get0(); }
    bool isEmpty() const              { return !data; }
    void clear()                      { data.reset(); }
    void init()                       { if (!data) reinit(); }
    void reinit()                     { anFill(*this); }
    
    T& create()                       { return create<T>(); }

    template<typename U>
    U& create()                       { U* u = new U; data.reset(u); return *u; }
    
    template<typename U>
    void produce(U&& u)               { anProduce(*this, u); }

    template<typename U>
    void copy(const An<U>& a)         { data.reset(new U(*a.data)); }

private:
    T* get0() const
    {
        const_cast<An*>(this)->init();
        return data.get();
    }

    std::shared_ptr<T> data;
};

template<typename T>
void anFill(An<T>& a)
{
    throw std::runtime_error(std::string("Cannot find implementation for interface: ")
            + typeid(T).name());
}

template<typename T>
struct AnAutoCreate : An<T>
{
    AnAutoCreate()     { create(); }
};

template<typename T>
T& single()
{
    static T t;
    return t;
}

template<typename T>
An<T> anSingle()
{
    return single<AnAutoCreate<T>>();
}

#define PROTO_IFACE(D_iface, D_an)    \
    template<> void anFill<D_iface>(An<D_iface>& D_an)

#define DECLARE_IMPL(D_iface)    \
    PROTO_IFACE(D_iface, a);

#define BIND_TO_IMPL(D_iface, D_impl)    \
    PROTO_IFACE(D_iface, a) { a.create<D_impl>(); }

#define BIND_TO_SELF(D_impl)    \
    BIND_TO_IMPL(D_impl, D_impl)

#define BIND_TO_IMPL_SINGLE(D_iface, D_impl)    \
    PROTO_IFACE(D_iface, a) { a = anSingle<D_impl>(); }

#define BIND_TO_SELF_SINGLE(D_impl)    \
    BIND_TO_IMPL_SINGLE(D_impl, D_impl)

#define BIND_TO_IFACE(D_iface, D_ifaceFrom)    \
    PROTO_IFACE(D_iface, a) { anFill<D_ifaceFrom>(a); }

#define BIND_TO_PROTOTYPE(D_iface, D_prototype)    \
    PROTO_IFACE(D_iface, a) { a.copy(anSingle<D_prototype>()); }

Если описать вкратце, то объект An представляет из себя «умный» указатель, который автоматически заполняется при обращении к нему используя функцию anFill. Мы будем перегружать эту функцию для нужного нам интерфейса. Для создания объекта на основе входных данных используется функция anProduce, использование которой будет описано в разделе, посвященный фабрикам.

Шаблон «мост»


Начнем с самого простого и распространенного случая: скрыть данные объекта, оставив для использования только интерфейс. Таким образом, при изменении данных, например, добавлении одного поля в класс, нет необходимости перекомпилировать все, что использует этот класс. Этот шаблон проектирования называется «мост», еще говорят об pimpl-идиоме. Этот подход часто используется для разделения интерфейса от реализации.

// header file

// базовый класс всех интерфейсов
struct IObject
{
    virtual ~IObject() {}
};

struct IFruit : IObject
{
    virtual std::string getName() = 0;
};
// декларация заливки реализации для класса IFruit
DECLARE_IMPL(IFruit)

// cpp file
struct Orange : IFruit
{
    virtual std::string getName()
    {
        return "Orange";
    }
};
// связывание интерфейса IFruit с реализацией Orange
BIND_TO_IMPL(IFruit, Orange)

Первым делом создадим класс IObject, чтобы в каждом абстрактном классе не писать виртуальный деструктор. Дальше просто наследуем каждый интерфейс (абстракный класс) от нашего IObject. Интерфейс IFruit содержит единственную функцию getName() в качестве иллюстрации подхода. Вся декларация происходит в заголовочном файле. Конкретная реализация записывается уже в cpp файле. Здесь мы определяем нашу функцию getName() и затем связываем наш интерфейс с реализацией. При изменении изменении класса Orange достаточно перекомпилировать один файл.

Посмотрим на использование:

An<IFruit> f;
std::cout << "Name: " << f->getName() << std::endl;

// output
Name: Orange

Здесь мы просто создаем объект An, а затем при первоначальном доступе создается объект с нужной реализацией, которая описана в файле cpp. Время жизни контролируется автоматически, т.е. по выходу из функции объект автоматически уничтожится.

Шаблон «фабрика»


Теперь поговорим о наиболее распространенном шаблоне: фабричный метод или просто фабрика. Здесь я не буду приводить примеров, как обычно используется фабрика. Вы это можете почитать, например, в Википедии. Я собираюсь показать несколько иное использование этого шаблона проектирования.

Различие использования состоит в том, что для пользователя он остается невидимым в большинстве случаев. Но это не означает, что будут иметь место какие-либо ограничения. В статье будет продемонстрирована гибкость и сила предлагаемого подхода.

Для этого поставим задачу: необходимо создавать различные объекты в зависимости от входных параметров функции. Производящая функция, вообще говоря, может иметь несколько параметров. Однако без огранчения общности можно считать, что любую функцию с несколькими параметрами можно свести к функции с одним параметром, где в качестве аргумента используется структура с необходимыми входными данными. Поэтому мы везде и всюду будем использовать функцию с одним параметром для упрощения интерфейсов и понимания. Интересующиеся могут использовать variadic templates из нового стандарта c++0x, правда компиляторы msvc и icc их, к сожалению, пока не поддерживают.

Итак, перед нами стоит задача создать реализацию интерфейса IFruit в зависимости от типа фрукта FruitType:

enum FruitType
{
    FT_ORANGE,
    FT_APPLE
};

Для этого нам потребуется дополнительная реализация для Apple:

// cpp file
struct Apple : IFruit
{
    virtual std::string getName()
    {
        return "Apple";
    }
};

Создаем производящую функцию:

void anProduce(An<IFruit>& a, FruitType type)
{
    switch (type)
    {
    case FT_ORANGE:
        a.create<Orange>();
        break;

    case FT_APPLE:
        a.create<Apple>();
        break;
        
    default:
        throw std::runtime_error("Unknown fruit type");
    }
}

Данная функция автоматически вызывается при вызове метода An::produce, как показано ниже:

An<IFruit> f;
f.produce(FT_ORANGE);
std::cout << f->getName() << std::endl;
f.produce(FT_APPLE);
std::cout << f->getName() << std::endl;

// output:
Orange
Apple

Часто бывает полезно создавать объекты в завимости не от значений времени исполнения, т.е. в каждый конкретный момент времени мы явно знаем, какой объект хотим создать. Тогда можно использовать другие, более простые способы создания. Первый способ заключается в создании промежуточных объектов — тегов:

// теги для реализаций в заголовочном файле
struct OrangeTag {};
struct AppleTag {};

// реализация производящих функций в cpp файле
void anProduce(An<IFruit>& a, OrangeTag)
{
    a.create<Orange>();
}

void anProduce(An<IFruit>& a, AppleTag)
{
    a.create<Apple>();
}

// использование
An<IFruit> f;
f.produce(AppleTag());
std::cout << f->getName() << std::endl;
f.produce(OrangeTag());
std::cout << f->getName() << std::endl;

// output
Apple
Orange

Второй вариант заключается в создании специальных интерфейсов и использовании шаблона «мост»:

// header file
struct IOrange : IFruit {};
DECLARE_IMPL(IOrange)

struct IApple : IFruit {};
DECLARE_IMPL(IApple)

// cpp file
struct Orange : IOrange
{
    virtual std::string getName()
    {
        return "Orange";
    }
};
BIND_TO_IMPL(IOrange, Orange);

struct Apple : IApple
{
    virtual std::string getName()
    {
        return "Apple";
    }
};
BIND_TO_IMPL(IApple, Apple);

// использование
An<IOrange> o;
std::cout << "Name: " << o->getName() << std::endl;
An<IApple> a;
std::cout << "Name: " << a->getName() << std::endl;

// output
Name: Orange
Name: Apple


Шаблон «строитель»


Многие (в том числе и я) недоумевают, зачем нужен строитель, когда есть фабрика? Ведь по сути, это похожие шаблоны. Чем же они тогда отличаются?

Я их четко различаю следующим нехитрым способом: фабрика используется для создания экземпляра, тип которого зависит от передаваемых параметров. В то время, как строитель используется, когда тип известен, надо лишь по разному заполнять поля объекта. Т.е. фабрика создает различные типы, для строителя используется один и тот же тип, но с разным наполнением. Теперь приступим к примеру:

// header file
struct Fruit
{
    Fruit(const std::string& name) : m_name(name) {}
    std::string getName() { return m_name; }
    
private:
    std::string m_name;
};

// cpp file
struct Orange : Fruit
{
    Orange() : Fruit("Orange") {}
};

struct Apple : Fruit
{
    Apple() : Fruit("Apple") {}
};

enum FruitType
{
    FT_ORANGE,
    FT_APPLE
};

void anProduce(An<Fruit>& a, FruitType type)
{
    switch (type)
    {
    case FT_ORANGE:
        a.create<Orange>();
        break;
    case FT_APPLE:
        a.create<Apple>();
        break;
        
    default:
        throw std::runtime_error("Unknown fruit type");
    }
}

Здесь мы имеем класс Fruit, который уже не абстрактный. Он содержит знакомый нам метод getName(), который просто извлекает из содержимого класса нужный тип. Задача строителя — правильно заполнить это поле. Для этого используются 2 класса, конструкторы которых заполняют это поле правильным значением. Производящая функция anProduce создает нужный экземпляр, конструктор которого проделывает всю необходимую работу:

An<Fruit> f;
f.produce(FT_ORANGE);
std::cout << f->getName() << std::endl;
f.produce(FT_APPLE);
std::cout << f->getName() << std::endl;

// output
Orange
Apple


Шаблон «абстрактная фабрика»


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

Предположим, что нам необходимо создавать объекты GUI:

struct IWindow : IObject
{
    virtual std::string getWindowName() = 0;
};

struct IButton : IObject
{
    virtual std::string getButtonName() = 0;
};

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

struct IWindowsManager : IObject
{
    virtual void produceWindow(An<IWindow>& a) = 0;
    virtual void produceButton(An<IButton>& a) = 0;
};

Теперь декларируем реализации:

struct GtkWindow : IWindow
{
    virtual std::string getWindowName()
    {
        return "GtkWindow";
    }
};

struct GtkButton : IButton
{
    virtual std::string getButtonName()
    {
        return "GtkButton";
    }
};

struct GtkWindowsManager : IWindowsManager
{
    virtual void produceWindow(An<IWindow>& a)    { a.create<GtkWindow>(); }
    virtual void produceButton(An<IButton>& a)    { a.create<GtkButton>(); }
};
BIND_TO_IMPL_SINGLE(IWindowsManager, GtkWindowsManager)

И создаем производящие функции:

PROTO_IFACE(IWindow, a)
{
    An<IWindowsManager> pwm;
    pwm->produceWindow(a);
}

PROTO_IFACE(IButton, a)
{
    An<IWindowsManager> pwm;
    pwm->produceButton(a);
}

Теперь можно использовать наши интерфейсы:

An<IButton> b;
std::cout << b->getWindowName() << std::endl;
An<IWindow> w;
std::cout << w->getButtonName() << std::endl;

// output
GtkButton
GtkWindow

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

enum ManagerType
{
    MT_GTK,
    MT_UNKNOWN
};

// наша конфигурация
struct Configuration
{
    // по умолчанию используем неизвестный фреймворк
    Configuration() : wmType(MT_UNKNOWN) {}
    
    ManagerType wmType;
};
// связываем конфигурацию с единственным экземпляром (синглтоном)
BIND_TO_SELF_SINGLE(Configuration)

// класс создает нужные фабрики объектов в зависимости от конфигурации
struct WindowsManager
{
    // прописываем явные зависимости от синглтонов, см [1]
    An<IWindowsManager> aWindowsManager;
    An<Configuration> aConfiguration;
    
    WindowsManager()
    {
        switch (aConfiguration->wmType)
        {
        case MT_GTK:
            aWindowsManager.create<GtkWindowsManager>();
            break;
            
        default:
            throw std::runtime_error("Unknown manager type");
        }
    }
};
BIND_TO_SELF_SINGLE(WindowsManager)

// реализация создания IWindow
PROTO_IFACE(IWindow, a)
{
    An<WindowsManager> wm;
    wm->aWindowsManager->produceWindow(a);
}

// реализация создания IButton
PROTO_IFACE(IButton, a)
{
    An<WindowsManager> wm;
    wm->aWindowsManager->produceButton(a);
}

// использование
An<Configuration> conf;
conf->wmType = MT_GTK;    // будем использовать gtk

An<IButton> b;
std::cout << b->getButtonName() << std::endl;
An<IWindow> w;
std::cout << w->getWindowName() << std::endl;

// output
GtkButton
GtkWindow


Шаблон «прототип»


Данный шаблон позволяет создавать сложные или “тяжелые” объекты путем клонирования уже существующего объекта. Часто этот шаблон используется совместо с шаблоном синглтон, которых хранит клонируемый объект. Рассмотрим пример:

// header file
struct ComplexObject
{
    std::string name;
};
// декларация заливки реализации для класса ComplexObject
DECLARE_IMPL(ComplexObject)

// cpp file
struct ProtoComplexObject : ComplexObject
{
    ProtoComplexObject()
    {
        name = "ComplexObject from prototype";
    }
};
// связывание создания ComplexObject с ProtoComplexObject используя прототип
BIND_TO_PROTOTYPE(ComplexObject, ProtoComplexObject)

Здесь у нас есть некий сложный и тяжелый класс ComplexObject, который нам необходимо создавать. Создаем данный класс путем копирования объекта ProtoComplexObject, который забирается из синглтона:

#define BIND_TO_PROTOTYPE(D_iface, D_prototype)    \
    PROTO_IFACE(D_iface, a) { a.copy(anSingle<D_prototype>()); }

Теперь можно использовать прототип следующим образом:

An<ComplexObject> o;
std::cout << o->name << std::endl;

// output
ComplexObject from prototype


Шаблон «мультитон»


Предположим, что нам необходимо создавать соединения до датацентров, чтобы, например, получать необходимую информацию. Чтобы не перегружать датацентр мы должны использовать только одно соединение к каждому датацентру. Если бы у нас был один единственный датацентр, то мы бы использовали синглтон и использовали его каждый раз для передачи сообщения/запроса. Однако у нас есть 2 идентичных датацентра и мы хотим балансировать нагрузку между ними, т.е. по возможности использовать оба датацентра. Поэтому здесь синглтон не подходит, а подходит мультитон, который позволяет поддерживать несколько экземпляров объекта:

// header
// описание интерфейса соединения
struct IConnection : IObject
{
    virtual void send(const Buffer& buf) = 0;
    virtual Buffer recieve(size_t bytes) = 0;
};
// декларация заливки реализации
DECLARE_IMPL(IConnection)

// cpp file
// реализация соединения до датацентра
struct DataCenterConnection : IConnection
{
    DataCenterConnection()
    {
        std::cout << "Creating new connection" << std::endl;
        // ...
    }
    
    ~DataCenterConnection()
    {
        std::cout << "Destroying connection" << std::endl;
        // ...
    }
    
    // реализация recieve & send
    // ...
};

// менеджер, который управляет всеми соединениями до датацентров
struct ConnectionManager
{
    ConnectionManager() : connectionCount(0), connections(connectionLimit)
    {
    }
    
    void fillConnection(An<IConnection>& connection)
    {
        std::cout << "Filling connection: " << connectionCount + 1 << std::endl;
        if (connectionCount < connectionLimit)
        {
            // создаем новое соединение
            connections[connectionCount].create<DataCenterConnection>();
        }
        // используем уже созданные соединения
        connection = connections[connectionCount ++ % connectionLimit];
    }
    
private:
    // максимальное количество соединений
    static const size_t connectionLimit = 2;
    
    // текущее количество запрошенных соединений
    size_t connectionCount;
    std::vector<An<IConnection>> connections;
};
// связываем менеджер с единственным экземпляром
BIND_TO_SELF_SINGLE(ConnectionManager)

// реализация создания IConnection
PROTO_IFACE(IConnection, connection)
{
    An<ConnectionManager> manager;
    manager->fillConnection(connection);
}

// использование
for (int i = 0; i < 5; ++ i)
{
    An<IConnection> connection;
    connection->send(...);
}

// output
Filling connection: 1
Creating new connection
Filling connection: 2
Creating new connection
Filling connection: 3
Filling connection: 4
Filling connection: 5
Destroying connection
Destroying connection

Для реализации использовался наиболее простой алгоритм балансировки соединений: каждый новый запрос на использование соединения перенаправляется на следующий датацентр. Этого достаточно, чтобы проиллюстрировать действие этого шаблона проектирования: в самом начале создаются 2 соединения, а затем они повторно используются для новых объектов соединения. По завершении программы они автоматически уничтожаются.

Синглтон, фабрика и прототип


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

Итак, приступим. Для начала создадим интерфейсы и объекты, которые хотелось бы создавать:

struct IShape : IObject
{
    virtual std::string getShapeName() = 0;
    virtual int getLeftBoundary() = 0;
};

struct Square : IShape
{
    Square()                            { std::cout << "Square ctor" << std::endl; }
    Square(const Square& s)             { std::cout << "Square copy ctor" << std::endl; }

    virtual std::string getShapeName()  { return "Square"; }
    virtual int getLeftBoundary()       { return m_x; }
    
private:
    // upper left vertex
    int m_x;
    int m_y;
    // size of square
    int m_size;
};

struct Circle : IShape
{
    Circle()                            { std::cout << "Circle ctor" << std::endl; }
    Circle(const Circle& s)             { std::cout << "Circle copy ctor" << std::endl; }

    virtual std::string getShapeName()  { return "Circle"; }
    virtual int getLeftBoundary()       { return m_x - m_radius; }
    
private:
    // center of the circle
    int m_x;
    int m_y;
    // its radius
    int m_radius;
};

Я дополнил классы неким функционалом, который нам не потребуется, чтобы все выглядело «по-взрослому». Для быстрого поиска мы будем использовать unordered_map, который можно найти либо в boost, либо в std, если ваш компилятор поддерживает новый стандарт. Ключом будет являться строка, обозначающая тип, а значением — объект, который порождает необходимый экземпляр заданного типа. Для этого создадим соответствующие интерфейсы:

// интерфейс создания объекта нужного типа
template<typename T>
struct ICreator : IObject
{
    virtual void create(An<T>& a) = 0;
};

// реализация, создающая тип T_impl в качестве реализации интерфейса T
template<typename T, typename T_impl>
struct AnCreator : ICreator<T>
{
    virtual void create(An<T>& a)        { a.create<T_impl>(); }
};

// реализация, создающая тип T_impl в качестве реализации интерфейса T,
// использующая шаблон прототип, доставаемый из синглтона
template<typename T, typename T_impl>
struct AnCloner : ICreator<T>
{
    virtual void create(An<T>& a)        { a.copy(anSingle<T_impl>()); }
};

Т.к. у нас планируется создание тяжелых объектов, то в фабрике будем использовать AnCloner.

struct ShapeFactory
{
    ShapeFactory()
    {
        std::cout << "ShareFactory ctor" << std::endl;
        // заполнение контейнера для быстрого поиска ICreator и создания нужного типа
        add<Square>("Square");
        add<Circle>("Circle");
    }
    
    template<typename T>
    void add(const std::string& type)
    {
        // AnCloner создает объекты посредством использования прототипа
        // AnAutoCreate автоматически заполняет нужную реализацию в An<ICreator<...>>
        m_creator.insert(std::make_pair(type, AnAutoCreate<AnCloner<IShape, T>>()));
    }

    void produce(An<IShape>& a, const std::string& type)
    {
        auto it = m_creator.find(type);
        if (it == m_creator.end())
            throw std::runtime_error("Cannot clone the object for unknown type");
        it->second->create(a);
    }
    
private:
    std::unordered_map<std::string, An<ICreator<IShape>>> m_creator;
};
// связываем фабрику с синглтоном для "ленивости"
BIND_TO_SELF_SINGLE(ShapeFactory)

Итак, фабрика готова. Теперь переведем дух и добавим последнюю функцию для порождения объектов:

void anProduce(An<IShape>& a, const std::string& type)
{
    An<ShapeFactory> factory;
    factory->produce(a, type);
}

Теперь фабрику можно использовать:

std::cout << "Begin" << std::endl;
An<IShape> shape;
shape.produce("Square");
std::cout << "Name: " << shape->getShapeName() << std::endl;
shape.produce("Circle");
std::cout << "Name: " << shape->getShapeName() << std::endl;
shape.produce("Square");
std::cout << "Name: " << shape->getShapeName() << std::endl;
shape.produce("Parallelogram");
std::cout << "Name: " << shape->getShapeName() << std::endl;

Что даст вывод на экран:

Begin
ShareFactory ctor
Square ctor
Square copy ctor
Name: Square
Circle ctor
Circle copy ctor
Name: Circle
Square copy ctor
Name: Square
Cannot clone the object for unknown type

Рассмотрим подробнее, что у нас происходит. В самом начале выводится Begin, что означает, что никаких объектов еще не создано, включая фабрику и наши прототипы, говорящее о “ленивости” происходящего. Далее вызов shape.produce(«Square») порождает целую цепочку действий: создается фабрика (ShareFactory ctor), затем рождается прототип Square (Square ctor), затем прототип копируется (Square copy ctor) и возвращается нужный объект. На нем зовется метод getShapeName(), возвращающий строку Square (Name: Square). Аналогичный процесс происходит и с объектом Circle, только теперь фабрика уже создана и повторного создания и инициализации теперь не требуется. При последующем создании Square посредством shape.produce(«Square») теперь вызывается только копирование прототипа, т.к. сам прототип уже создан (Square copy ctor). При попытке создания неизвестной фигуры shape.produce(«Parallelogram») вызывается исключение, которое перехватывается в обработчике, опущенном для краткости (Cannot clone the object for unknown type).

Выводы


В этой статье рассмотрены порождающие шаблоны проектирования и их использование в различных ситуациях. Данная статья не претендует на полноту изложения таких шаблонов. Здесь я хотел продемонстрировать несколько иной взгляд на известные вопросы и задачи, возникающие при на этапе проектирования и реализации. Этот подход использует очень важный принцип, который лежит в основе всего, что описано в этой статье: принцип обращения зависимостей [7]. Для большей наглядности и понимания я поместил использование различных шаблонов в единую таблицу.

Таблица сравнения: безусловное создание экземпляров
Шаблон Обычное использование Использование в статье
Синглтон
T::getInstance()
An<T> ->
Мост
T::createInstance()
An<T> ->
Фабрика
T::getInstance().create()
An<T> ->
Мультитон
T::getInstance(instanceId)
An<T> ->

Таблица сравнения: создание экземпляров на основе входных данных
Шаблон Обычное использование Использование в статье
Фабрика
T::getInstance().create(...)
An<T>.produce(...)
Абстрактная фабрика
U::getManager().createT(...)
An<T>.produce(...)
Прототип
T::getInstance().clone()
An<T>.produce(...)
Синглтон, прототип и фабрика
T::getInstance().getPrototype(...).clone()
An<T>.produce(...)

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

Что дальше?


А дальше — список литературы. Ну а в следующей статье будут рассмотрены вопросы многопоточности и другие интересные и необычные «плюшки».

Литература


[1] Хабрахабр: Использование паттерна синглтон
[2] Хабрахабр: Синглтон и время жизни объекта
[3] Википедия: Фабричный метод
[4] Википедия: Порождающие шаблоны проектирования
[5] Andrey on .NET: Порождающие шаблоны
[6] Andrey on .NET: Фабричный метод
[7] Wikipedia: Dependency inversion principle
[8] Википедия: Обращение контроля
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+39
Comments 42
Comments Comments 42

Articles