📜 ⬆️ ⬇️

Dependency Reversal and Generating Design Patterns

annotation


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


Introduction


Many probably have heard, read, or even used generative design patterns [4] . This article will be about them. However, the emphasis here will be on other things. Of course, this article can be used as a reference for generating patterns, or as an introduction to them. But my final goal is somewhat in a different plane, namely, in the plane of using these patterns in real code.

It is no secret that many, having learned about patterns, try to start using them everywhere. However, not all so simple. Many articles on this topic do not pay enough attention to their use in the code. And when they begin to fasten the templates to the code, then something so inconceivable arises that neither to say in a fairy tale nor to describe with a pen. I have seen various incarnations of these ideas, sometimes you involuntarily ask yourself: what was the author smoking? Take, for example, a factory or factory method from Wikipedia [3] . I will not give all the code, I will give 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, and how to use it in real life, then immediately the following observations 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 factory 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 does everything.
  3. It is assumed that objects are created by the operator new. Here the question immediately arises with the processing of exceptional situations and the lifetime of the object.

Anyway, and this example is just some illustration that contains many flaws. In real life, this is not used.

“What are they using then?”, The attentive reader will ask. Below is the use code. This list does not claim 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 singletones. You may also notice that when creating objects, the ears of the used templates stick out. During the subsequent refactoring, this will be felt from the unpleasant side. Often used approach when returning objects to the pointer. So taught in all books, so the code continues to be written. If everything is clear with the createObject method - you need to call delete at the end, what to do with the configuration? Is it singleton or not? If yes, then nothing needs to be done. And if not? Questions arise again with a lifetime. We should not forget about the correct handling of exceptions either, and such code with exception handling causes problems associated with cleaning up resources.

Whatever one may say, but I would like to have a unified approach, which would run like a red thread through the generated objects and did not differ in the various ways of creation, of which there are many. In order to put this into practice, we will use the powerful principle of reversing dependencies [7] . Its essence is that some kind of abstraction is introduced, an interface. Next, using and using the code is connected through the entered interface using, for example, call control [8] . This allows code that wants to create objects to abstract from the specifics of creating a class and simply use a dedicated interface. All care falls on the shoulders of the functional that implements this interface. The article discusses in detail how to create objects using almost all known generating design patterns, and also gives an example when several generating patterns are used to create instances at the same time. An example of a singleton is described in detail in a previous article [2] ; in this article, it will only be used together in other templates.

Infrastructure


An object 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 further narrative. For more details, see previous article [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>()); } 

In brief, the An object is a “smart” pointer, which is automatically filled when it is accessed 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 pattern


Let's start with the simplest and most common case: hide the 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 a “bridge”, also referred to as a pimpl idiom. This approach is often used to separate the interface from the 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, create an IObject class so that in each abstract class we don’t write a virtual destructor. Then just inherit each interface (abstract 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 concrete implementation is already recorded in the cpp file. Here we define our getName () function and then associate our interface with the implementation. When changing the class of Orange, it is enough to recompile one file.

Let's look at using:

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

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

Template "factory"


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 this, 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 set the task: it is necessary to create different objects depending on the input parameters of the function. A generating function, generally speaking, can have several parameters. However, without limiting the generality, we can assume that any function with several parameters can be reduced to a function with a single parameter, where the 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 interfaces and understanding. Those interested can use the variadic templates of the new standard c ++ 0x, although the compilers msvc and icc, unfortunately, do not support them yet.

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

 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 generating function:

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

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

 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 

It is often useful to create objects not depending on the time of execution; at any given time we clearly know what object we want to create. Then you can use other, more simple ways to create. The first way is to create intermediate objects - tags:

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

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


Builder pattern


Many (including me) are wondering why a builder is needed when there is a factory? Indeed, in essence, these are similar templates. How do they differ then?

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 being transferred. While the builder is used when the type is known, you only need to fill in the fields of the object in different ways. Those. the factory creates different types, for the builder the same type is used, but with different content. Now let's take 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<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"); } } 

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

 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 


Abstract Factory Template


This template is used in case of need to create a set of objects with some kind of affinity. 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 working 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<IWindow>& a) = 0; virtual void produceButton(An<IButton>& a) = 0; }; 

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

And create generating functions:

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

Now you can use our interfaces:

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

Let's complicate the example. Suppose we need to choose a framework depending on the configuration. We look, how it 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<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 


Prototype template


This template allows you to create complex or “heavy” objects by cloning an already existing object. Often this template is used in conjunction with the singleton template, which 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 a complex and heavy class ComplexObject that we need to create. We create this class by copying a ProtoComplexObject object, which is taken from a singleton:

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

Now you can use the prototype as follows:

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


Template "multiton"


Suppose that we need to create connections to data centers, for example, to obtain the necessary information. In order not to overload the data center, we must use only one connection to each data center. If we had one single data center, then we would use 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, here the singleton is not suitable, but the multi-tone is suitable, which allows you to maintain several 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<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 

For implementation, the simplest connection balancing algorithm was used: each new request for using a connection is redirected to the next data center. This is enough to illustrate the effect of this design pattern: at the very beginning, 2 connections are created, 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 generating patterns. Suppose we need to create different objects depending on the value being passed. The number of different types created is assumed to be quite large, so I want to use a reasonably quick way to select the desired type, that is want to use 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. Each prototype wants to generate lazily, i.e. do not create a prototype, there is no need for them yet. It is also possible that this functionality will never be used, so you don’t want to create an object for generation in advance, i.e. We will create a "lazy" factory.

So let's get started. To begin with, we will create 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 added classes with some functionality that we do not need so that everything looks “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 the type, and the value will be the object that generates the necessary instance of the specified type. To do this, create the appropriate interfaces:

 //      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>()); } }; 

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

 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) 

So the factory is ready. Now we’ll translate the spirit and add the last function for spawning objects:

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

Now the factory can be used:

 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; 

What will display on 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 here. At the very beginning, Begin is displayed, which means that no objects have yet been created, including the factory and our prototypes, which speak of the “laziness” of what is happening. Then the call to shape.produce (“Square”) creates a whole chain of actions: a factory is created (ShareFactory ctor), then the Square prototype is born (Square ctor), then the prototype is copied (Square copy ctor) and the necessary 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 has already been created and re-creation and initialization is no longer required. During the subsequent creation of Square by means of shape.produce ("Square"), now only the prototype copying is called, since the prototype itself has already been created (Square copy ctor).When attempting to create an unknown shape.produce ("Parallelogram"), an exception is thrown, which is caught in a handler omitted for brevity (Cannot clone the object for unknown type).

findings


This article discusses generating design patterns and their use in various situations. This article does not pretend to the complete presentation of such templates. Here I would like to demonstrate a slightly different view on known issues and problems arising during the design and implementation stages. This approach uses a very important principle that underlies everything that is described in this article: the principle of reversing dependencies [7] . For greater clarity and understanding, I put the use of different templates in a single table.

Comparison Table: unconditional instantiation
TemplateNormal useUse in article
Singleton
 T::getInstance() 
 An<T> -> 
Bridge
 T::createInstance() 
 An<T> -> 
Factory
 T::getInstance().create() 
 An<T> -> 
Multiton
 T::getInstance(instanceId) 
 An<T> -> 

Comparison table: creating instances based on input data
TemplateNormal useUse in article
Factory
 T::getInstance().create(...) 
 An<T>.produce(...) 
Abstract Factory
 U::getManager().createT(...) 
 An<T>.produce(...) 
Prototype
 T::getInstance().clone() 
 An<T>.produce(...) 
Singleton, prototype and factory
 T::getInstance().getPrototype(...).clone() 
 An<T>.produce(...) 

The advantages are obvious: the implementation does not penetrate the interfaces . Such an approach allows the code using to abstract away from the specific way of creating instances and concentrate on the problem being solved. This, in turn, allows you to create very flexible applications, adding the ability to easily change the implementation without having to refactor the corresponding code.

What's next?


And then - a list of references. Well, in the next article questions of multithreading and other interesting and unusual "buns" will be considered.

Literature


[1] Habrahabr: Using the singleton pattern
[2] Habrahabr: Singleton and object 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: Handling Control

Source: https://habr.com/ru/post/118550/


All Articles