Рубріки: Теорія

Що таке патерн Singleton: навіщо він потрібен та як його використовувати

Андрій Денисенко

Що таке патерн Singleton

Патерн «одинак» (Singleton, синглетон, синглет) належить до породжуючих патернів проєктування, тобто реалізує один із підходів до створення об’єкта.

Singleton — це клас, який гарантує, що існує один і лише один екземпляр класу, і надає глобальну точку доступу до нього.

Цей екземпляр створюється синглетоном «за лаштунками», тому класу-клієнту немає необхідності його створювати. Екземпляр створюється лише тоді, коли в ньому виникає потреба. За дотримання цього обмеження відповідає клас-одинак, а не клас-клієнт.

До цього єдиного екземпляру можна звертатися безпосередньо через глобальну точку доступу, що є статичним методом, який повертає цей об’єкт. Якщо до моменту виклику екземпляр не створено, то його буде створено. Якщо на момент виклику екземпляр існує, цей метод повертає його. Це називається лінивою або відкладеною ініціалізацією (lazy initialization).

Призначення патерну Singleton

Призначення 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:

Останні статті

Айтівець Міноборони США понабирав кредитів і хотів продати рф секретну інформацію

32-річний розробник безпеки інформаційних систем Агентства національної безпеки Джарех Себастьян Далке отримав 22 роки в'язниці…

30.04.2024

Простий та дешевий. Українська Flytech запустила масове виробництво розвідувальних БПЛА ARES

Українська компанія Flytech представила розвідувальний безпілотний літальний апарат ARES. Основні його переваги — недорога ціна…

30.04.2024

Запрошуємо взяти участь у премії TechComms Award. Розкажіть про свій потужний PR-проєкт у сфері IT

MC.today разом з Асоціацією IT Ukraine і сервісом моніторингу та аналітики згадок у ЗМІ та…

30.04.2024

«Йдеться про потенціал мобілізації»: Україна не планує примусово повертати українців із ЄС

Україна не буде примусово повертати чоловіків призовного віку з-за кордону. Про це повідомила у Брюсселі…

30.04.2024

В ЗСУ з’явився жіночий підрозділ БПЛА — і вже можна проходити конкурсний відбір

В Збройних Силах України з'явився жіночий підрозділ з БПЛА. І вже проводиться конкурсний відбір до…

30.04.2024

GitHub на наступному тижні випустить Copilot Workplace — ШІ-помічника для розробників

GitHub анонсував Copilot Workspace, середовище розробки з використанням «агентів на базі Copilot». За задумкою, вони…

30.04.2024