📜 ⬆️ ⬇️

Rake 1: Rise of the lonely phoenixes

I wanted to write an article about the theoretical shortcomings of the Singleton pattern, but a brief search showed that there was enough material on this topic. But it seems to me that there are not enough real examples of architectural problems with loners. I will try to fill this gap with the help of this post. At the end, conclusions from their own mistakes will be given, which so far make it possible to avoid repeating problems.

Rake

So, for the beginning of the actual rake. In the inherited project, single classes were actively used. There were more than ten of them. Dynamic creation of instances was used during the first access, i.e. the getInstance method had the following form

Singleton* Singleton::getInstance() { if (sInstance == NULL) { sInstance = new Singleton; } return sInstance; } 

And then began the hunt for memory leaks. The destruction of singles was not provided, because the profiler gave a lot of false "leaks", among which it was almost impossible to find a real problem. Yes, and did not want to look. Therefore, it was decided to write the destroyInstance method, which would allow to destroy instances of singles classes upon request.

The situation was greatly simplified by the fact that the implementation of Singleton-logic was rendered into a separate template class - there was no need for a global rework. It was enough to add a deleting method and its call for each single ... And it was here that the theory worked. Basically two of its points
  1. Dependencies on the classes of singles are not obvious from the interfaces of the classes and modules.
  2. The singleton class has no explicit owner. That's why he is a loner. From that and suffers.

Having called all destroyInstance, I opened the profiler in anticipation of the hunt for real leaks and ... all plunged into the same sea of ​​false positives! Most of the singles lived in their own pleasure and further, despite the destruction experienced. The first thing, as always, was the compiler. And, as always, the real problem was in its own hands.
')
Everything is properly destroyed. But uncontrollable dependencies existed between singles. Often, the other was used in the destructor of one. And since getInstance not only returned an existing instance of the class, but also created new ones if necessary, mass uncontrolled uprisings from the ashes began to occur. Immediately I remembered the book Alexandrescu [1]. Only, unlike me, he was engaged in necromancy (in section 6.6, which is almost symbolic) intentionally.

The situation needed to be corrected. The first thought is to expose a flag when destroying an instance that prevents the object from being re-created. But this led to the need for a large-scale modification of the already tangled code that was written with the expectation that getInstance always returns the correct pointer. As a result, realizing that there was no easy solution, I almost completely eradicated the loners (leaving one), which I still only enjoy.

findings

The problems associated with managing the lifetime of singleton classes are often raised and discussed. It seems to me that the root of the problem is in misunderstanding the essence of this pattern. The singleton should be alone . And that is all. Yes, as a result, more careful design is required, identifying dependencies between entities. But it's worth it. Because in the end it turns out a single point of management of the application, the interfaces and logic are clarified.

Of course, there are certainly applications where this approach is unacceptable. But these are exceptions, and they should be treated as exceptions. If the application is one, then the class completely independent in terms of its creation and destruction should be one.

For example, the classic example of a keyboard, display and log [1]. The owner of these classes can be made a global Application. The name is beaten, but here are its pluses


The only problem is the uncontrolled creation / copying of these classes. So, theoretically, the programmer may not find that the keyboard control object needs to be obtained from the Application, and create its own instance. This development can be easily stopped by declaring all unwanted designers closed. This will make impossible the "manual" creation. For the owner class to still instantiate a keyboard object, it’s enough to declare it a friend.
 class Keyboard { private: Keyboard(); friend class Application; }; 

Such implementation will play an important informational role. Even without any documentation, simply by examining the interface of the Keyboard class, the developer will be able to understand that you need to contact Application for instances.

A single class can almost always be replaced by a better solution. At least, in my case over the past year, every attempt to make another singleton with more detailed analysis invariably turned out to be a manifestation of laziness and unwillingness to once again think about the planned architecture.

[1] Alexandrescu A. Modern C ++ Design: Generalized Programming and Applied Design Patterns.
[2] atexit
[3] KISS

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


All Articles