Using the singleton pattern
Introduction
Many are already familiar with a term such as singleton. In short, this is a pattern that describes an object that has a single instance. There are many ways to create such an instance. But now it’s not about that. I will also omit issues related to multithreading, although this is a very interesting and important question when using this pattern. I would like to tell you about the correct use of singleton.
If you read the literature on this topic, you can come across various criticisms of this approach. Here is a list of disadvantages [1] :
- Singleton violates SRP (Single Responsibility Principle) - the singleton class, in addition to fulfilling its immediate responsibilities, is also involved in controlling the number of copies.
- The dependency of a regular class on a singleton is not visible in the public class contract. Since usually an instance of a singleton is not passed in the method parameters, but is obtained directly through getInstance (), to find out the dependence of a class on a singleton, you need to get into the body of each method - just looking at the object’s public contract is not enough. As a result: the complexity of refactoring during the subsequent replacement of a singleton with an object containing several instances.
- Global status. Everyone seems to already know about the dangers of global variables, but this is the same problem. When we access an instance of a class, we do not know the current state of this class, and who and when changed it, 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 accesses to it, which causes the implicit dependence of the subsystems on each other and, as a result, seriously complicates the development.
- The presence of a singleton reduces the testability of the application as a whole and classes that use singleton in particular. Firstly, instead of a singleton, you cannot push a Mock-object, and secondly, if a singleton has an interface for changing its state, then the tests begin to depend on each other.
Thus, when these problems are present, many conclude that the use of this pattern should be avoided. In general, I agree with the above problems, but I do not agree that, based on these problems, we can conclude that it is not worth using singletones. Let's take a closer look at what I mean and how to avoid these problems even when using a singleton.
Implementation
The first thing I would like to note: singleton is an implementation, not an interface. What does it mean? This means that the class should, whenever possible, use a certain interface, otherwise, whether the singleton will be there or not, it does not know and should not know, because any explicit use of singleton will lead to 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 somehow fill the implementation into the class, while the class using the interface does not care about who will do it and when. These questions do not interest him at all. All he needs to know is how to properly use the provided functionality. The functional interface in this case can be either an abstract interface or a specific class. In our particular case, it doesn’t matter.
There is an idea, let's implement it in C ++. Here, templates and the possibility of their specialization will help us. First, we define a class that will contain a pointer to the required instance:
template
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(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 with the desired instance in the absence of one (reinit method). When accessing the class, the instance is automatically initialized and called. Let's look at the implementation of the anFill function:
template
void anFill(An& a)
{
throw std::runtime_error(std::string("Cannot find implementation for interface: ")
+ typeid(T).name());
}
Thus, by default, this function throws an exception to prevent the use of an 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 various contexts. To do this, we specialize the anFill function for our class X:
template<>
void anFill(An& 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 (multithreading issues will be discussed in another article). Now we can use class X as follows:
An x;
x->action();
Or easier:
An()->action();
What will display:
1: in action
When the action is called again, we will see:
2: in action
Which means that we still have the state and the instance of class X is exactly one. Now let's complicate a little example. To do this, create a new class Y that will contain the use of class X:
struct Y
{
An x;
void doAction() { x->action(); }
};
Now if we want to use the default instance, then we can just do the following:
Y y;
y.doAction();
What after 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;
y.x = &x;
y.doAction();
Those. we populate class Y with our (known) instance and call the corresponding function. On the screen we get:
1: in action
We will now examine the case of abstraction interfaces. Create an abstract base class:
struct I
{
virtual ~I() {}
virtual void action() = 0;
};
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 populate using the first implementation of Impl1:
template<>
void anFill(An& a)
{
static Impl1 i;
a = &i;
}
So the following code:
An i;
i->action();
Will give a conclusion:
in Impl1
Create a class using our interface:
struct Z
{
An i;
void doAction() { i->action(); }
};
Now we want to change the implementation. Then do the following:
Z z;
Impl2 i;
z.i = &i;
z.doAction();
Which results in:
in Impl2
Idea development
In general, this could be finished. However, it’s worth adding some useful macros to make life easier:
#define PROTO_IFACE(D_iface) \
template<> void anFill(An& 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(); }
#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, it is part of the language and it can be used, besides I am not subject to dogma and prejudice.
The DECLARE_IMPL macro declares a padding other than the default padding. In fact, this line says that for this class there will be an automatic filling with a certain value in the absence of 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
T& single()
{
static T t;
return t;
}
Using the macro BIND_TO_SELF_SINGLE means that an instance of it will be used for the class. Obviously, in the case of an abstraction class, this macro is not applicable and you must use BIND_TO_IMPL_SINGLE with the specification of the class implementation. This implementation can be hidden and declared only in the CPP file.
Now consider the use of a specific example, for example, 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 conf;
void connect()
{
if (m_connectionCount == conf->getConnectionsLimit())
throw std::runtime_error("Number of connections exceeds the limit");
...
}
private:
int m_connectionCount;
};
conclusions
As a result, I would note the following:
- Explicit job of dependency on an interface: now you don’t need to look for dependencies, they are all written in the class declaration and this is part of its interface.
- Providing access to a singleton instance and the class interface are separated into different objects. Thus, everyone solves his problem, thereby preserving SRP.
- If there are several configurations, you can easily fill the required instance into the ConnectionManager class without any problems.
- Testability of the class: you can make a mock-object and check, for example, the condition of the condition when the connect method is called:
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 indicated at the beginning of this article. In subsequent articles, I would like to address important issues related to lifetime and multithreading.
Literature
[1] RSDN forum: singleton flaw list
[2] Wikipedia: singleton
[3] Inside C ++: singleton