Що таке патерн Singleton: навіщо він потрібен та як його використовувати
Зміст
Що таке патерн Singleton
Патерн «одинак» (Singleton, синглетон, синглет) належить до породжуючих патернів проєктування, тобто реалізує один із підходів до створення об’єкта.
Singleton — це клас, який гарантує, що існує один і лише один екземпляр класу, і надає глобальну точку доступу до нього.
Цей екземпляр створюється синглетоном «за лаштунками», тому класу-клієнту немає необхідності його створювати. Екземпляр створюється лише тоді, коли в ньому виникає потреба. За дотримання цього обмеження відповідає клас-одинак, а не клас-клієнт.
До цього єдиного екземпляру можна звертатися безпосередньо через глобальну точку доступу, що є статичним методом, який повертає цей об’єкт. Якщо до моменту виклику екземпляр не створено, то його буде створено. Якщо на момент виклику екземпляр існує, цей метод повертає його. Це називається лінивою або відкладеною ініціалізацією (lazy initialization).
Призначення патерну Singleton
Призначення Singleton таке:
- він гарантує, що створюється лише один екземпляр класу;
- він надає глобальну точку доступу до свого єдиного екземпляра.
Також патерн Singleton можна використовувати, коли об’єкт має бути доступним для створення підкласів і клієнтам необхідно використовувати розширений клас без зміни свого коду.
Мотиваціями до застосування патерну Singleton є приклади з реального світу, які у своїй системі існують лише в одному екземплярі, наприклад:
- клас, що представляє файлову систему чи базу даних;
- аналого-цифровий перетворювач;
- компанія, для якої створено систему бухобліку;
- уряд країни;
- а також інші приклади, зокрема ведення журналів, драйвери, кешування, пул ниток тощо.
Структура
Клас-одинак містить щонайменше:
- приватне статичне поле, у якому зберігається єдиний екземпляр класу;
- конструктор, оголошений як приватний, щоб неможливо було створити клас із використанням цього конструктора;
- публічний статичний метод, який повертає (і перед цим за необхідності створює) екземпляр класу.
Плюси та мінуси 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.
Скотт Майєрс
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
Якщо вам знадобиться контроль над видаленням синглетонів, його можна забезпечити за допомогою додаткового класу. Цей клас відповідатиме лише за руйнування синглетону й матиме доступ до деструктора. Цей спосіб запропонував Джон Вліссідес (один із «банди чотирьох»).
Джон Вліссідес
Клас 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:
Favbet Tech – це ІТ-компанія зі 100% українською ДНК, що створює досконалі сервіси для iGaming і Betting з використанням передових технологій та надає доступ до них. Favbet Tech розробляє інноваційне програмне забезпечення через складну багатокомпонентну платформу, яка здатна витримувати величезні навантаження та створювати унікальний досвід для гравців.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: