Зміст
Патерн «одинак» (Singleton, синглетон, синглет) належить до породжуючих патернів проєктування, тобто реалізує один із підходів до створення об’єкта.
Singleton — це клас, який гарантує, що існує один і лише один екземпляр класу, і надає глобальну точку доступу до нього.
Цей екземпляр створюється синглетоном «за лаштунками», тому класу-клієнту немає необхідності його створювати. Екземпляр створюється лише тоді, коли в ньому виникає потреба. За дотримання цього обмеження відповідає клас-одинак, а не клас-клієнт.
До цього єдиного екземпляру можна звертатися безпосередньо через глобальну точку доступу, що є статичним методом, який повертає цей об’єкт. Якщо до моменту виклику екземпляр не створено, то його буде створено. Якщо на момент виклику екземпляр існує, цей метод повертає його. Це називається лінивою або відкладеною ініціалізацією (lazy initialization).
Призначення Singleton таке:
Також патерн Singleton можна використовувати, коли об’єкт має бути доступним для створення підкласів і клієнтам необхідно використовувати розширений клас без зміни свого коду.
Мотиваціями до застосування патерну Singleton є приклади з реального світу, які у своїй системі існують лише в одному екземплярі, наприклад:
Клас-одинак містить щонайменше:
Переваги | Недоліки |
|
|
Існує кілька реалізацій патерну Singleton. Ми розглянемо класичну реалізацію, реалізацію Майєрса та покращену версію класичної реалізації.
Класичну версію патерну Singleton було представлено 1994 року в книзі Design Patterns. Elements of Reusable Object-Oriented Software (Еріх Гамма, Річард Хелм, Ральф Джонсон, Джон Вліссідес).
Велика четвірка (автори книги)
Інтерфейс
Singleton.h
class Singleton { public: static Singleton* getInstance(); protected: Singleton(); private: static Singleton* _instance; };
Реалізація
Singleton.cpp
Singleton* Singleton::_instance = 0; Singleton* Singleton::getInstance () { if (_instance == 0) { _instance = new Singleton; } return _instance; }
У класичній версії шаблону реалізовано захищений конструктор, приватне статичне поле для покажчика та статичний метод для його отримання. При першому зверненні цей спосіб динамічно виділяє необхідний обсяг пам’яті та повертає покажчик на цей блок пам’яті.
Нижче наведено новішу версію цієї реалізації (для неї потрібен C++11). У цій версії забороняється кілька операцій:
#include <iostream> class ClassicSingleton{ private: static ClassicSingleton* _instance; ClassicSingleton() = default; ~ClassicSingleton() = default; public: ClassicSingleton(const ClassicSingleton&) = delete; ClassicSingleton& operator=(const ClassicSingleton&) = delete; static ClassicSingleton* getInstance(){ if ( !_instance ){ _instance = new ClassicSingleton(); } return _instance; } }; ClassicSingleton* ClassicSingleton::_instance = nullptr; int main(){ std::cout << std::endl << "Classic Singleton Demo" << std::endl; std::cout << "1st getInstance() call: "<< ClassicSingleton::getInstance() << std::endl; std::cout << "2nd getInstance() call: "<< ClassicSingleton::getInstance() << std::endl; std::cout << std::endl; std::cin; }
Вивід програми показує, що існує лише один екземпляр класу ClassicSingleton
:
У C++17 оголошення й визначення статичної змінної екземпляру можна дати безпосередньо в класі:
#include <iostream> class ClassicSingleton{ private: inline static ClassicSingleton* instance{nullptr}; ClassicSingleton() = default; ~ClassicSingleton() = default; public: ClassicSingleton(const ClassicSingleton&) = delete; ClassicSingleton& operator=(const ClassicSingleton&) = delete; static ClassicSingleton* getInstance(){ if ( !instance ){ instance = new ClassicSingleton(); } return instance; } };
Оскільки в класичній реалізації повертається покажчик на екземпляр класу, відповідальність за очищення пам’яті несе користувач. Щоб уникнути проблем зі звільненням пам’яті, Скотт Майєрс запропонував іншу реалізацію патерну Singleton.
Скотт Майєрс
Після завершення програми C++ автоматично знищує статичні об’єкти. Можна скористатися цим, щоб клас сам відповідав за звільнення пам’яті, яку відведено для екземпляру. До того ж ця реалізація автоматично підтримує багатопоточність.
Статичні змінні локальної області видимості створюються під час першого використання. Така відкладена ініціалізація є гарантією, яку надає C++98. Singleton Майєрса засновано саме на цій ідеї. Замість статичного екземпляра класу Singleton у ньому є локальна статична змінна з типом Singleton:
class MeyersSingleton{ private: MeyersSingleton() = default; ~MeyersSingleton() = default; public: MeyersSingleton(const MeyersSingleton&) = delete; MeyersSingleton& operator = (const MeyersSingleton&) = delete; static MeyersSingleton& getInstance(){ static MeyersSingleton instance; return instance; } };
Але й реалізація Мейєрса має недоліки. По-перше, з використанням цієї реалізації складно створювати об’єкти похідних класів. По-друге, порядок видалення синглетонів залишається невизначеним.
Якщо вам знадобиться контроль над видаленням синглетонів, його можна забезпечити за допомогою додаткового класу. Цей клас відповідатиме лише за руйнування синглетону й матиме доступ до деструктора. Цей спосіб запропонував Джон Вліссідес (один із «банди чотирьох»).
Джон Вліссідес
Клас Singleton можна визначити так:
class Singleton { public: static Singleton *Instance(); protected: Singleton(){} friend class SingletonDestroyer; virtual ~Singleton(){} private: static Singleton *_instance; static SingletonDestroyer _destroyer; }; Singleton *Singleton::_instance = 0; SingletonDestroyer Singleton::_destroyer; Singleton *Singleton::Instance() { if (!_instance) { _instance = new Singleton; _destroyer.SetSingleton(_instance); } return _instance; }
А ось код його руйнівника:
class SingletonDestroyer { public: SingletonDestroyer(Singleton * = 0); ~SingletonDestroyer(); void SetSingleton(Singleton *s); private: Singleton *_singleton; }; SingletonDestroyer::SingletonDestroyer(Singleton *s) { _singleton = s; } SingletonDestroyer::~SingletonDestroyer() { delete _singleton; } void SingletonDestroyer::SetSingleton(Singleton *s) { _singleton = s; }
У класі Singleton оголошено статичний член SingletonDestroyer
, який автоматично створюється під час запуску програми. Якщо і коли користувач вперше викликає Singleton::Instance
, об’єкт Singleton
створюється й передається статичному об’єкту SingletonDestroyer
. У такий спосіб SingletonDestroyer
стає власником об’єкта.
Під час виходу з програми буде зруйновано SingletonDestroyer
, а разом із ним і Singleton. Тепер руйнування Singleton відбувається неявно.
SingletonDestroyer
оголошено в класі синглетону як friend
. Це зроблено для надання доступу до захищеного деструктора класу Singleton.
Проблеми порядку створення й руйнування екземплярів набувають особливої ваги, коли в програмі використовується кілька синглетонів. Вище ми розглянули один із способів вирішення питання щодо руйнування об’єкта. У цьому розділі ми наводимо код, у якому реалізовано керування порядком створення об’єктів.
TwoSingletons.h
class SingletonAuto { private: SingletonAuto() { } SingletonAuto( const SingletonAuto& ); SingletonAuto& operator=( SingletonAuto& ); public: static SingletonAuto& getInstance() { static SingletonAuto instance; return instance; } }; class SingletonMain { private: SingletonMain( SingletonAuto& instance): s1( instance) { } SingletonMain( const SingletonMain& ); SingletonMain& operator=( SingletonMain& ); SingletonAuto& s1; public: static SingletonMain& getInstance() { static SingletonMain instance( SingletonAuto::getInstance()); return instance; } };
TwoSingletons.cpp
#include "TwoSingletones.h" int main() { SingletonMain& s = SingletonMain::getInstance(); return 0; }
Синглетон SingletonAuto
створюється автоматично під час ініціалізації SingletonMain
викликом SingletonAuto::getInstance()
.
Паттерн Singleton потрібно використовувати лише тоді, коли він є необхідним. Пам’ятайте, що синглетон має представляти об’єкт, який є єдиним у системі.
А взагалі синглетонів рекомендовано уникати. Причина в тому, що це замасковані складні глобальні об’єкти. Існує багато реалізацій синглетонів, і це також проблема.
Якщо ви не хочете, щоб глобальний об’єкт змінювався, оголосіть його як const
або constexpr
.
Але існує і виняток: використовуйте найпростіший синглетон (настільки простий, що його часто можна не вважати синглетоном), щоб зробити ініціалізацію під час першого використання, якщо така ситуація взагалі складається:
X& myX() { static X my_x {3}; return my_x; }
Це одне з найкращих рішень проблеми порядку ініціалізації. У багатопотоковому середовищі ініціалізація статичного об’єкта не спричиняє стан гонки (якщо не реалізується доступ до загальнодоступного об’єкта з його конструктора).
Але якщо знищення X передбачає операцію, яку потрібно синхронізувати, необхідно використовувати складніше рішення. Наприклад, таке:
X& myX() { static auto p = new X {3}; return * p; // potential leak }
Тепер клієнту потрібно видалити цей об’єкт, орієнтуючись на безпеку потоку. Це може спричинити появу помилок, тому не користуйтеся цим способом, крім випадків, коли:
myX
знаходиться у багатопотоковому коді;X
слід знищити (наприклад, для звільнення ресурсу);X
потрібно синхронізувати.На закінчення наведемо рекомендації щодо очищення коду від непотрібних синглетонів. Таке очищення — завдання складне, але здійсненне:
singleton
;Як і інші шаблони, Singleton потрібно застосовувати саме тоді, коли у ньому є потреба. Якщо правильно використовувати цей патерн, він може значно підвищити продуктивність і зменшити споживання ресурсів у вашому застосунку.
Щоб закріпити матеріал, радимо переглянути корисні відео про використання Singleton у трьох популярних мовах програмування:
С++:
Java:
Python:
Резиденти Дія.City сплатили до бюджету понад 8 млрд грн податків в І кварталі 2025 року.…
У Китаї закликають офісних працівників не працювати надто багато — держава сподівається, що вільний час…
Експерти звертають увагу на тривожну тенденцію: люди все частіше використовують ChatGPT, щоб визначити місцезнаходження, зображене…
Компанія JetBrains випустила нову версію мультимовного середовища розробки IntelliJ IDEA 2025.1. Оновлена IDE отримала численні…
Платформа обміну миттєвими повідомленнями Discord впроваджує функцію перевірки віку за допомогою сканування обличчя. Зараз вона…
Wikipedia намагається захистити себе від тисяч різноманітних ботів-скрейперів, які сканують дані цієї платформи для навчання…