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

Что такое паттерн 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:

 

 

 

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

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023