⬆️ ⬇️

Using the singleton pattern

Introduction



Many are already familiar with such a term as singleton. In brief, this is a pattern that describes an object that has a single instance. You can create such an instance in different ways. But now we are not talking about this. I will also omit issues related to multithreading, although this is a very interesting and important issue when using this pattern. I would like to tell you about the proper use of singleton.



If you read the literature on this topic, then you can meet various criticisms of this approach. I will give a list of shortcomings [1] :

  1. Singleton violates SRP (Single Responsibility Principle) - the singleton class, in addition to fulfilling its direct duties, is also engaged in controlling the number of its copies.
  2. The dependence of the usual class from the singleton is not visible in the public contract of the class. Since usually a singleton instance is not passed in the method parameters, but is obtained directly through getInstance (), then to identify the dependence of the class on the singleton, it is necessary to crawl into the body of each method — simply viewing the public contract of the object is not enough. As a consequence: the difficulty of refactoring during the subsequent replacement of a singleton with an object containing several instances.
  3. Global state About the harm of global variables, it seems everyone already knows, but here is the same problem. When we access an instance of a class, we do not know the current state of this class, and who changed it when, and this state may not be at all as expected. In other words, the correctness of working with a singleton depends on the order of calls to it, which causes an implicit dependence of the subsystems on each other and, as a result, seriously complicates the development.
  4. The presence of a singleton reduces the testability of the application in general and the classes that use the singleton, in particular. Firstly, instead of a singleton one cannot push a Mock-object, and secondly, if Singlelton has an interface for changing its state, then tests begin to depend on each other.


Thus, in the presence of these problems, many conclude that the use of this pattern should be avoided. In general, I agree with the above problems, but I do not agree with the fact that, based on these problems, we can conclude that you should not use singletones. Let's take a closer look at what I mean and how to avoid these problems even when using singleton.



Implementation



The first thing I would like to point out: a singleton is an implementation, not an interface. What does it mean? This means that the class should, if possible, use some kind of interface, and whether there will be a singleton or not, it does not know and should not know it, because any explicit use of singleton will result in these problems. In words, it looks good, let's see how it should look in life.



To implement this idea, we will use a powerful approach called Dependency Injection. Its essence is that we in some way fill the implementation into a class, while the class that uses the interface does not care about who will do it and when. He is not interested in these questions at all. All he needs to know is how to properly use the provided functionality. The interface of the functional can be both an abstract interface and a specific class. In our particular case, it does not matter.

')

The idea is, let's implement in C ++. Here templates and the possibility of their specialization will help us. First, let's define a class that will contain a pointer to the required instance:

template<typename T> struct An { An() { clear(); } T* operator->() { return get0(); } const T* operator->() const { return get0(); } void operator=(T* t) { data = t; } bool isEmpty() const { return data == 0; } void clear() { data = 0; } void init() { if (isEmpty()) reinit(); } void reinit() { anFill(*this); } private: T* get0() const { const_cast<An*>(this)->init(); return data; } T* data; }; 


The described class solves several problems. First, it stores a pointer to the required instance of the class. Secondly, in the absence of an instance, the anFill function is called, which fills it with the required instance if there is no such instance (the reinit method). When a class is accessed, it is automatically initialized by the instance and invoked. Let's look at the implementation of the anFill function:

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


Thus, by default, this function throws an exception in order to prevent the use of undeclared function.



Examples of using



Now suppose we have a class:

 struct X { X() : counter(0) {} void action() { std::cout << ++ counter << ": in action" << std::endl; } int counter; }; 


We want to make it a singleton for use in different contexts. To do this, we specialize the anFill function for our class X:

 template<> void anFill<X>(An<X>& a) { static X x; a = &x; } 


In this case, we used the simplest singleton and for our reasoning the concrete implementation does not matter. It is worth noting that this implementation is not thread-safe (issues of multithreading will be discussed in another article). Now we can use class X as follows:

 An<X> x; x->action(); 


Or easier:

 An<X>()->action(); 


What will display:

 1: in action 


When we call action again, we will see:

 2: in action 


That says that at us the state and a copy of a class X exactly one is saved. Now let's complicate a bit of an example. To do this, create a new class Y, which will contain the use of class X:

 struct Y { An<X> x; void doAction() { x->action(); } }; 


Now, if we want to use the default instance, then we can simply do the following:

 Y y; y.doAction(); 


What after the previous calls will display:

 3: in action 


Now suppose we wanted to use another instance of the class. This is very easy to do:

 X x; yx = &x; y.doAction(); 


Those. we fill the class Y with our (known) instance and call the corresponding function. On the screen we get:

 1: in action 


Let us now consider the case with abstract interfaces. Create an abstract base class:

 struct I { virtual ~I() {} virtual void action() = 0; }; 


We define 2 different implementations of this interface:

 struct Impl1 : I { virtual void action() { std::cout << "in Impl1" << std::endl; } }; struct Impl2 : I { virtual void action() { std::cout << "in Impl2" << std::endl; } }; 


By default, we will fill in using the first implementation of Impl1:

 template<> void anFill<I>(An<I>& a) { static Impl1 i; a = &i; } 


Thus, the following code:

 An<I> i; i->action(); 


Will conclude:

 in Impl1 


Create a class using our interface:

 struct Z { An<I> i; void doAction() { i->action(); } }; 


Now we want to change the implementation. Then do the following:

 Z z; Impl2 i; zi = &i; z.doAction(); 


What gives as a result:

 in Impl2 




Idea development



In general, this could be finished. However, it is worth adding a few useful macros to make life easier:

 #define PROTO_IFACE(D_iface) \ template<> void anFill<D_iface>(An<D_iface>& a) #define DECLARE_IMPL(D_iface) \ PROTO_IFACE(D_iface); #define BIND_TO_IMPL_SINGLE(D_iface, D_impl) \ PROTO_IFACE(D_iface) { a = &single<D_impl>(); } #define BIND_TO_SELF_SINGLE(D_impl) \ BIND_TO_IMPL_SINGLE(D_impl, D_impl) 


Many may say that macros are evil. I declare responsibly that I am familiar with this fact. Nevertheless, this is part of the language and it can be used, besides, I am not subject to dogma and prejudice.



The macro DECLARE_IMPL declares a different fill than the default fill. In fact, this line says that for this class there will be an automatic filling with a certain value if there is no explicit initialization. The macro BIND_TO_IMPL_SINGLE will be used in the CPP file for implementation. It uses the single function, which returns a singleton instance:

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


The use of the BIND_TO_SELF_SINGLE macro indicates that an instance of itself will be used for the class. Obviously, in the case of an abstract class, this macro is not applicable and you must use BIND_TO_IMPL_SINGLE with the task of implementing the class. This implementation can be hidden and declared only in a CPP file.



Now consider the use of a specific example, such as configuration:

 // IConfiguration.hpp struct IConfiguration { virtual ~IConfiguration() {} virtual int getConnectionsLimit() = 0; virtual void setConnectionLimit(int limit) = 0; virtual std::string getUserName() = 0; virtual void setUserName(const std::string& name) = 0; }; DECLARE_IMPL(IConfiguration) // Configuration.cpp struct Configuration : IConfiguration { Configuration() : m_connectionLimit(0) {} virtual int getConnectionsLimit() { return m_connectionLimit; } virtual void setConnectionLimit(int limit) { m_connectionLimit = limit; } virtual std::string getUserName() { return m_userName; } virtual void setUserName(const std::string& name) { m_userName = name; } private: int m_connectionLimit; std::string m_userName; }; BIND_TO_IMPL_SINGLE(IConfiguration, Configuration); 


Further it can be used in other classes:

 struct ConnectionManager { An<IConfiguration> conf; void connect() { if (m_connectionCount == conf->getConnectionsLimit()) throw std::runtime_error("Number of connections exceeds the limit"); ... } private: int m_connectionCount; }; 




findings



As a result, I would note the following:

  1. Explicit assignment of interface dependencies: now there is no need to look for dependencies, they are all written into the class declaration and this is part of its interface.
  2. Providing access to the singleton instance and the class interface are separated into different objects. Thus, each solves his problem, thereby preserving the SRP.
  3. If there are multiple configurations, you can easily fill the desired instance into the ConnectionManager class without any problems.
  4. Testability class: you can make a mock object and check, for example, that the condition is working correctly when you call the connect method:

     struct MockConfiguration : IConfiguration { virtual int getConnectionsLimit() { return 10; } virtual void setConnectionLimit(int limit) { throw std::runtime_error("not implemented in mock"); } virtual std::string getUserName() { throw std::runtime_error("not implemented in mock"); } virtual void setUserName(const std::string& name) { throw std::runtime_error("not implemented in mock"); } }; void test() { // preparing ConnectionManager manager; MockConfiguration mock; manager.conf = &mock; // testing try { manager.connect(); } catch(std::runtime_error& e) { //... } } 




Thus, the described approach eliminates the problems mentioned at the beginning of this article. In subsequent articles, I would like to touch on important issues related to lifetime and multithreading.



Literature



[1] Forum RDN: list of disadvantages of a singleton

[2] Wikipedia: Singleton

[3] Inside C ++: Singleton

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



All Articles