Dependency handling and generic design patterns

    annotation


    This is the third article enlightened on generative design patterns and related issues. Here we will look at favorite techniques for creating objects: factories, factories , abstract factories, builders, prototypes, multitones, delayed initialization, as well as a little touch on the pimpl idiom or template “bridge”. The use of singletones was discussed in detail in the first [1] and second [2] articles, however, as you will see later, singletones are often used in conjunction with other design patterns.


    Introduction


    Many probably heard, read, or even used generative design patterns [4] . This article will be discussed just about them. However, the emphasis will be on other things. Of course, this article can be used as a guide to generative patterns, or as an introduction to them. But my ultimate goal is in a slightly different plane, namely, in the plane of using these templates in real code.

    It's no secret that many, having learned about the templates, try to start using them everywhere. However, not all so simple. Many articles on this topic do not pay due attention to their use in code. And when they begin to fasten the templates to the code, then something so unimaginable arises that neither in a fairy tale nor with a pen to describe. I have seen various incarnations of these ideas; sometimes I involuntarily ask myself the question: what did the author smoke? Take, for example, a factory or factory method from Wikipedia [3] . I will not give all the code, I will only use:

    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];
    

    If you ask yourself how to use it in real life, then immediately the following remarks arise:
    1. How do I know that I need to use exactly the 0th or 1st element? They are no different.
    2. Suppose you need to create some elements in a loop. Where do I get the knowledge of where these factories are located? If I initialize the factories right there, then why do I need them at all? You can simply create an object and call a specific method or a stand-alone function that will do everything.
    3. Objects are assumed to be created by the new operator. This immediately raises the question of handling exceptional situations and the lifetime of an object.

    Like it or not, but this example is just a kind of illustration that contains many flaws. In real life, this is not used.

    “What then is used?”, The attentive reader will ask. Below is the usage code. This list is not intended to be complete:
    // создание объекта, используя фабрику, получаемую из синглтона
    Object* o = Factory::getInstance().createObject("object_name");
    // использование конфигурации для создания объектов
    Configuration* conf = Configuration::getCurrentConfiguration();
    Object* o = Factory::getInstance().createObject(conf->getObjectNameToCreate());
    

    It is worth noting that factories in "real life" are usually singleton. You can also notice that when creating objects, the ears stick out of the patterns used. With subsequent refactoring, this will make itself felt from an unpleasant side. An approach is often used when returning objects by pointer. So taught in all books, the code continues to be written. If everything is clear with the createObject method - you need to call delete at the end, then what to do with the configuration? Is it singleton or not? If so, then nothing needs to be done. And if not? Again, questions arise with a lifetime. You should not forget about the correct handling of exceptions, and such code with exception handling causes problems associated with cleaning resources.

    Like it or not, I would like to have a unified approach that would pass through the generated objects with a red thread and not distinguish between various creation methods, of which there are many. In order to implement this, we will use the powerful principle of dependency reversal [7] . Its essence is that a certain abstraction, an interface, is introduced. Further, the using and used code is connected through the entered interface using, for example, the control call [8]. This allows the code that wants to create objects to abstract from the specifics of creating the class and simply use the dedicated interface. All care rests on the shoulders of the functional that implements this interface. The article describes in detail how to create objects using almost all known generative design patterns, as well as an example where several generative patterns are used to create instances at the same time. An example of a singleton is described in detail in a previous article [2] , this article will only use it jointly in other templates.

    Infrastructure


    Object An and the infrastructure around it are described in detail in the second article [2] . Here I will only give the code that will be used in the subsequent narration. For details, see the previous article [2] .

    template
    struct An
    {
        template
        friend struct An;
        An()                              {}
        template
        An(const An& a) : data(a.data) {}
        template
        An(An&& 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(); }
        template
        U& create()                       { U* u = new U; data.reset(u); return *u; }
        template
        void produce(U&& u)               { anProduce(*this, u); }
        template
        void copy(const An& a)         { data.reset(new U(*a.data)); }
    private:
        T* get0() const
        {
            const_cast(this)->init();
            return data.get();
        }
        std::shared_ptr data;
    };
    template
    void anFill(An& a)
    {
        throw std::runtime_error(std::string("Cannot find implementation for interface: ")
                + typeid(T).name());
    }
    template
    struct AnAutoCreate : An
    {
        AnAutoCreate()     { create(); }
    };
    template
    T& single()
    {
        static T t;
        return t;
    }
    template
    An anSingle()
    {
        return single>();
    }
    #define PROTO_IFACE(D_iface, D_an)    \
        template<> void anFill(An& 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(); }
    #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(); }
    #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(a); }
    #define BIND_TO_PROTOTYPE(D_iface, D_prototype)    \
        PROTO_IFACE(D_iface, a) { a.copy(anSingle()); }
    

    In short, the An object is a “smart” pointer, which is automatically populated when accessing it using the anFill function. We will overload this function for the interface we need. To create an object based on input data, the anProduce function is used, the use of which will be described in the section on factories.

    Bridge Template


    Let's start with the simplest and most common case: hide object data, leaving only the interface to use. Thus, when changing data, for example, adding one field to a class, there is no need to recompile everything that uses this class. This design pattern is called the "bridge", they also talk about the pimpl idiom. This approach is often used to separate an interface from an implementation.

    // 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)
    

    First of all, we will create the IObject class so that we do not write a virtual destructor in each abstract class. Next, we simply inherit each interface (abstraction class) from our IObject. The IFruit interface contains a single getName () function to illustrate the approach. The entire declaration takes place in the header file. The specific implementation is already written in the cpp file. Here we define our getName () function and then bind our interface to the implementation. When changing a change to the Orange class, it is enough to recompile one file.

    Let's look at the use of:

    An f;
    std::cout << "Name: " << f->getName() << std::endl;
    // output
    Name: Orange
    

    Here we simply create an An object, and then upon initial access, an object is created with the desired implementation, which is described in the cpp file. The lifetime is controlled automatically, i.e. upon exiting the function, the object is automatically destroyed.

    Factory Template


    Now let's talk about the most common pattern: factory method or just factory. Here I will not give examples of how the factory is usually used. You can read it, for example, on Wikipedia . I am going to show a slightly different use of this design pattern.

    The difference in use is that for the user it remains invisible in most cases. But this does not mean that there will be any restrictions. The article will demonstrate the flexibility and strength of the proposed approach.

    To do this, we pose the problem: it is necessary to create various objects depending on the input parameters of the function. A generating function, generally speaking, can have several parameters. However, without loss of generality, we can assume that any function with several parameters can be reduced to a function with one parameter, where a structure with the necessary input data is used as an argument. Therefore, we will use the function with one parameter everywhere and everywhere to simplify the interfaces and understanding. Those interested can use variadic templates from the new c ++ 0x standard, although the msvc and icc compilers, unfortunately, do not yet support them.

    So, we are faced with the task of creating an implementation of the IFruit interface, depending on the type of fruit FruitType:

    enum FruitType
    {
        FT_ORANGE,
        FT_APPLE
    };
    

    To do this, we need an additional implementation for Apple:

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

    Create a producing function:

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

    This function is automatically called when the An :: produce method is called, as shown below:

    An f;
    f.produce(FT_ORANGE);
    std::cout << f->getName() << std::endl;
    f.produce(FT_APPLE);
    std::cout << f->getName() << std::endl;
    // output:
    Orange
    Apple
    

    It is often useful to create objects depending on non-runtime values, i.e. at any given moment in time, we clearly know which object we want to create. Then you can use other, simpler ways to create. The first way is to create intermediate objects - tags:

    // теги для реализаций в заголовочном файле
    struct OrangeTag {};
    struct AppleTag {};
    // реализация производящих функций в cpp файле
    void anProduce(An& a, OrangeTag)
    {
        a.create();
    }
    void anProduce(An& a, AppleTag)
    {
        a.create();
    }
    // использование
    An f;
    f.produce(AppleTag());
    std::cout << f->getName() << std::endl;
    f.produce(OrangeTag());
    std::cout << f->getName() << std::endl;
    // output
    Apple
    Orange
    

    The second option is to create special interfaces and use the “bridge” template:

    // 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 o;
    std::cout << "Name: " << o->getName() << std::endl;
    An a;
    std::cout << "Name: " << a->getName() << std::endl;
    // output
    Name: Orange
    Name: Apple
    


    Builder Template


    Many (including myself) are perplexed, why do we need a builder when there is a factory? Indeed, in fact, these are similar patterns. How are they then different?

    I clearly distinguish them in the following simple way: the factory is used to create an instance, the type of which depends on the parameters passed. While the builder is used when the type is known, it is only necessary to fill in the fields of the object in different ways. Those. the factory creates different types, the builder uses the same type, but with different contents. Now let's start with an example:

    // 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& a, FruitType type)
    {
        switch (type)
        {
        case FT_ORANGE:
            a.create();
            break;
        case FT_APPLE:
            a.create();
            break;
        default:
            throw std::runtime_error("Unknown fruit type");
        }
    }
    

    Here we have the Fruit class, which is no longer abstract. It contains the familiar getName () method, which simply extracts the desired type from the contents of the class. The builder's task is to fill in this field correctly. For this, 2 classes are used, the designers of which fill this field with the correct value. The producing function anProduce creates the desired instance, the constructor of which does all the necessary work:

    An f;
    f.produce(FT_ORANGE);
    std::cout << f->getName() << std::endl;
    f.produce(FT_APPLE);
    std::cout << f->getName() << std::endl;
    // output
    Orange
    Apple
    


    Abstract Factory Template


    This template is used if it is necessary to create a set of objects that have a certain relationship. To illustrate this approach, consider the following example.

    Suppose we need to create GUI objects:

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

    At the same time, we have several frameworks that allow you to work with such objects, one of which is, for example, gtk. To do this, create an interface for generating objects:

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

    Now declare the implementation:

    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& a)    { a.create(); }
        virtual void produceButton(An& a)    { a.create(); }
    };
    BIND_TO_IMPL_SINGLE(IWindowsManager, GtkWindowsManager)
    

    And create the producing functions:

    PROTO_IFACE(IWindow, a)
    {
        An pwm;
        pwm->produceWindow(a);
    }
    PROTO_IFACE(IButton, a)
    {
        An pwm;
        pwm->produceButton(a);
    }
    

    Now you can use our interfaces:

    An b;
    std::cout << b->getWindowName() << std::endl;
    An w;
    std::cout << w->getButtonName() << std::endl;
    // output
    GtkButton
    GtkWindow
    

    Let's complicate the example. Let's say we need to choose a framework depending on the configuration. We look at how this can be implemented:

    enum ManagerType
    {
        MT_GTK,
        MT_UNKNOWN
    };
    // наша конфигурация
    struct Configuration
    {
        // по умолчанию используем неизвестный фреймворк
        Configuration() : wmType(MT_UNKNOWN) {}
        ManagerType wmType;
    };
    // связываем конфигурацию с единственным экземпляром (синглтоном)
    BIND_TO_SELF_SINGLE(Configuration)
    // класс создает нужные фабрики объектов в зависимости от конфигурации
    struct WindowsManager
    {
        // прописываем явные зависимости от синглтонов, см [1]
        An aWindowsManager;
        An aConfiguration;
        WindowsManager()
        {
            switch (aConfiguration->wmType)
            {
            case MT_GTK:
                aWindowsManager.create();
                break;
            default:
                throw std::runtime_error("Unknown manager type");
            }
        }
    };
    BIND_TO_SELF_SINGLE(WindowsManager)
    // реализация создания IWindow
    PROTO_IFACE(IWindow, a)
    {
        An wm;
        wm->aWindowsManager->produceWindow(a);
    }
    // реализация создания IButton
    PROTO_IFACE(IButton, a)
    {
        An wm;
        wm->aWindowsManager->produceButton(a);
    }
    // использование
    An conf;
    conf->wmType = MT_GTK;    // будем использовать gtk
    An b;
    std::cout << b->getButtonName() << std::endl;
    An w;
    std::cout << w->getWindowName() << std::endl;
    // output
    GtkButton
    GtkWindow
    


    Prototype Template


    This template allows you to create complex or “heavy” objects by cloning an existing object. Often this template is used in conjunction with the singleton template that the cloned object stores. Consider an example:

    // 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)
    

    Here we have some complex and heavy class ComplexObject, which we need to create. We create this class by copying the ProtoComplexObject object, which is taken from the singleton:

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

    Now you can use the prototype as follows:

    An o;
    std::cout << o->name << std::endl;
    // output
    ComplexObject from prototype
    


    Multiton Template


    Suppose we need to create connections to data centers in order, for example, to obtain the necessary information. In order not to overload the data center, we should use only one connection to each data center. If we had one single data center, then we would use a singleton and use it every time to send a message / request. However, we have 2 identical data centers and we want to balance the load between them, i.e. if possible, use both data centers. Therefore, a singleton is not suitable here, but a multiton is suitable, which allows you to support multiple instances of the object:

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

    For implementation, the simplest connection balancing algorithm was used: each new request for using the connection is redirected to the next data center. This is enough to illustrate the effect of this design pattern: 2 connections are created at the very beginning, and then they are reused for new connection objects. At the end of the program, they are automatically destroyed.

    Singleton, Factory and Prototype


    In the final example, consider the synergy of several generative patterns. Suppose we need to create different objects depending on the value passed. The number of different types being created is supposed to be quite large, so I want to use a fairly quick way to select the desired type, i.e. We want to use a search using hash functions. Each instance of the desired type will be quite heavy, so there is a need to use the prototype template to facilitate the creation of instances. I want to generate each prototype lazily, i.e. Do not create a prototype until they are needed. There is also a chance that they will never use this functionality, therefore, you do not want to create an object in advance to generate it, i.e. We will create a "lazy" factory.

    So let's get started. First, create the interfaces and objects that we would like to create:

    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;
    };
    

    I supplemented the classes with some functionality that we don’t need to make everything look “grown-up”. For a quick search, we will use unordered_map, which can be found either in boost or in std, if your compiler supports the new standard. The key will be a string denoting a type, and the value will be an object that spawns the required instance of a given type. To do this, create the appropriate interfaces:

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

    Because we plan to create heavy facilities, then in the factory we will use AnCloner.

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

    So, the factory is ready. Now take a breath and add the last function to generate objects:

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

    Now the factory can be used:

    std::cout << "Begin" << std::endl;
    An 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;
    

    What will give an output to the screen:

    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
    

    Let us consider in more detail what is happening with us. Begin is displayed at the very beginning, which means that no objects have yet been created, including the factory and our prototypes, which speaks of the “laziness” of what is happening. Further, the call to shape.produce (“Square”) generates a whole chain of actions: a factory (ShareFactory ctor) is created, then the prototype Square (Square ctor) is born, then the prototype is copied (Square copy ctor) and the desired object is returned. It calls the getShapeName () method, which returns the string Square (Name: Square). A similar process occurs with the Circle object, only now the factory is already created and re-creation and initialization is no longer required. The next time you create a Square using shape.produce ("Square"), now only the copy of the prototype is called, because the prototype itself has already been created (Square copy ctor).

    conclusions


    This article describes generative design patterns and their use in various situations. This article is not intended to be comprehensive in describing such patterns. Here I wanted to demonstrate a slightly different view of the known issues and tasks that arise during the design and implementation phase. This approach uses a very important principle that underlies everything that is described in this article: the principle of dependency handling [7] . For clarity and understanding, I put the use of various templates in a single table.

    Comparison Table: Unconditional Instance Creation
    TemplateNormal useUse in article
    Singleton
    T::getInstance()
    An ->
    Bridge
    T::createInstance()
    An ->
    Factory
    T::getInstance().create()
    An ->
    Multiton
    T::getInstance(instanceId)
    An ->

    Comparison Table: Creating Instances Based on Input
    TemplateNormal useUse in article
    Factory
    T::getInstance().create(...)
    An.produce(...)
    Abstract factory
    U::getManager().createT(...)
    An.produce(...)
    Prototype
    T::getInstance().clone()
    An.produce(...)
    Singleton, prototype and factory
    T::getInstance().getPrototype(...).clone()
    An.produce(...)

    The advantages are obvious: the implementation does not penetrate the interfaces . This approach allows the code to be abstracted from a specific way of creating instances and focus on the problem being solved. This, in turn, allows you to create very flexible applications, adding the ability to easily change the implementation without the need to refactor the appropriate code.

    What's next?


    And then a list of references. Well, in the next article, multithreading issues and other interesting and unusual “buns” will be considered.

    Literature


    [1] Habrahabr: Using the singleton pattern
    [2] Habrahabr: Singleton and the object's lifetime
    [3] Wikipedia: Factory method
    [4] Wikipedia: Generating design patterns
    [5] Andrey on .NET: Generating patterns
    [6] Andrey on .NET : Factory method
    [7] Wikipedia: Dependency inversion principle
    [8] Wikipedia: Inversion of control

    Also popular now: