Паттерн Singleton. Описание. Пример использования

В статье описывается паттерн Singleton (Одиночка), рассмотрены 2 реализации и некоторые возможные модификации. Проанализированы сильные и слабые стороны шаблона. Приведен пример использования.

Статья состоит из двух частей:

  1. описание шаблона, варианты реализации, проблемы. Часть должна быть понятна программистам на любых языках, хотя примеры приведены на С++;
  2. более сложный пример — решается следующая задача:

    Не во всех операционных системах возможна установка громкости звука для отдельного приложения — громкость устанавливается глобально для всей системы. Однако, такая регулировка иногда необходима. Усложняется задача тем, что объекты, издающие звуки, могут быть разбросаны по всему приложению — нужно как-то связать их с объектом громкости (существующем в единственном экземпляре).

Шаблон проектирования Singleton — описание, варианты реализации

Singleton применяется если должен существовать только один экземпляр некоторого класса, и этому экземпляру нужна глобальная точка доступа (возможность обратиться из любой точки программы).

Например, в шаблоне Producer-Consumer в единственном экземпляре должен существовать пул задач (поставщики добавляют задачи, а потребители — забирают на исполнение) [2]. Кроме того, Singleton нередко используется совместно с шаблонами Abstract Factory, Object Pool, Builder.

Возможны различные реализации шаблона Singlteton, их свойства будут во многом определяться используемым языком программирования в этой статье используется C++. В качестве примера возьмем класс SimpleClass и постараемся обеспечить для него существование единственного экземпляра и глобальной точки доступа.

// simpleclass.h
class SimpleClass {
public:
  SimpleClass();
  SimpleClass(int val);

  int get() const;
  void set(int val);
protected:
  int m_val;
};

// simpleclass.cpp
SimpleClass::SimpleClass() : m_val(0) { }

SimpleClass::SimpleClass(int val) : m_val(val) { }

int SimpleClass::get() const { return m_val; }

void SimpleClass::set(int val) { m_val = val; }

Самый простой вариант реализации требуемых свойств — добавить в наш класс статическую функцию-член, возвращающую тот самый единственный экземпляр.

// simpleclass.h
class SimpleClass {
public:
  static SimpleClass* instance();
//...
private:
  static SimpleClass* m_pinstance;
};

// simpleclass.cpp
SimpleClass* SimpleClass::m_pinstance = 0;

SimpleClass* SimpleClass::instance() {
  if(m_pinstance == 0)
     m_pinstance = new SimpleClass;
  return m_pinstance;
}

Теперь наш класс хранит экземпляр, который инициализируется при первом вызове SimpleClass::instance(), а при повторных вызовах — возвращается существующий объект. Если требуется запретить создание других объектов класса SimpleClass — конструкторы можно переместить в секцию private.

Другой вариант реализации Singleton заключается в использовании локальной статической переменной метода instance, хранящей единственный экземпляр класса (Синглтон Меерса).

// simpleclass.h
class SimpleClass {
public:
  int get() const;
  void set(int val);

  static SimpleClass& instance();
private:
  SimpleClass(int val);
  SimpleClass();

  int m_val;
};

//simpleclass.cpp
SimpleClass& SimpleClass::instance() {
  static SimpleClass instance;
  return instance;
}

Каждый из вариантов имеет как достоинства, так и недостатки, я попытался собрать все плохое, что пишут о Singleton, а дальше мы посмотрим как устранить часть недостатков:

  • если обращение к Singleton осуществляется из разных потоков — возможны состояния гонок;
  • информации для инициализации одиночки может быть недостаточно. Не в любой точке программы можно найти данные для правильной инициализации Одиночки [3];
  • у класса Одиночки должен быть конструктор по умолчанию;
  • между одиночками не могут существовать зависимости, т.к. порядок вызова конструкторов глобальных объектов, расположенных в различных единицах трансляции, не регламентируется в стандарте C++ [3];
  • состояние объекта Singleton зависит от порядка обращений к нему, это осложняет разработку [5];
  • добавить Singleton легко, но потом трудно от него избавиться;
  • Singleton нарушает принцип единой обязанности (Single Responsibility Principle, SRP), т.к. помимо своих непосредственных обязанностей класс следит за количеством своих экземпляров [4, 5];
  • в ряде случаев Singleton делает невозможным повторное использование класса — «в приложении X должен существовать единственный экземпляр объекта, но в приложении Y — в нескольких» [6];
  • зависимость класса от Одиночки трудно обнаружить — для этого не достаточно просмотреть определение класса, надо просмотреть реализацию каждого метода.

Обилие недостатков превращает Singleton в анти-паттерн, рекоментудется избегать его использования. Действительно, в наших приложениях существует не так много объектов, которые должны существовать в единственном экземпляре, но даже если такой объект есть — не обязательно требуется глобальная точка доступа к нему. Так, например, при реализации шаблона Producer/Consumer [2] мы обошлись без Singleton, хотя очередь задач и существовала в единственном экземпляре. Тем не менее, в некоторых случаях использование одиночки оправдано и может не приводить к тяжелым последствиям. Рассмотрим недостатки подробнее.

Гонки потоков при обращении к Singleton связаны с тем, что если объект еще не был создан и 2 потока одновременно выполнят операцию instance, то может быть создано несколько одиночек.

SimpleClass* SimpleClass::instance() { // 1
  if(m_pinstance == 0) // 2
     m_pinstance = new SimpleClass; // 3
  return m_pinstance; // 4
} // 5

Так например, первый поток может выполнить проверку условия из второй строки и быть прерван во время выполнения конструктора. Пока первый поток отдыхает, начинается работать второй поток, который выполняет тот же код, но в связи с тем, что первый поток не выполнял инициализации m_pinstance, второй поток также вызывает конструктор SimpleClass и возвращает указатель на созданный объект. После этого может быть активирован первый поток, который заканчивает создание своего экземпляра объекта и повторно инициализирует m_pinstance.

Для решения проблемы гонок можно использовать мьютекс (приведен пример с использованием библиотеки Qt, однако в настоящее время средства синхронизации потоков включены в стандартную библиотеку).

static SimpleClass* SimpleClass::instance() {
  static QMutex mutex;
  if (!m_pinstance) {
    mutex.lock();
    if (!m_pinstance)
      m_pinstance = new SimpleClass;
    mutex.unlock();
  }
  return m_pinstance;
}

С другой стороны, у реализации Singleton, использующей статическую локальную переменную метода instance, состояния гонок потоков возникнуть не может. Это гарантируется стандартом (цитата относится к инициализации локальных статических переменных):

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

При инициализации одиночки в нашем примере вызывается конструктор по умолчанию, однако, если требуется вызывать другой конструктор — метод instance может принимать соответствующие параметры. Для первой реализации (с указателем) возможно описать сразу несколько методов instance. Если же класс не имеет конструктора по умолчанию и для создания экземпляра нужны какие-либо данные, которых может не быть в некоторых частях программы — возможно вы используете Singleton неправильно или нужно более внимательно посмотреть на архитектуру приложения.

Большинство источников литературы указывает на то, что если в программе есть несколько Синглтонов и между ними имеются зависимости, то в программе на С++ будут проблемы в связи с тем, что «для глобальных объектов стандартом не определяется порядок вызова конструкторов через границы единиц трансляции» [3]. Чтобы понять о чем тут идет речь, можно посмотреть пример, обладающий заявленной проблемой (без Синглтона):

// first.h
struct A {
  static First first;
};

// second.h
struct B {
  static Second second;
}

// first.cpp
First::First() {

}

// second.cpp
Second::Second() {
  A::first.foo();
}

В приведенном коде, имеется 2 глобальных объекта A::first и B::second. Эти объекты расположены в различных единицах трансляции, поэтому порядок вызова их конструкторов не определен. Если первым будет вызван конструктор FIrst, а затем — Second — то проблема не возникнет. Однако, при ином порядке вызова мы получим неопределенное поведение (undefined behavior), т.к. конструктор Second попробует вызвать метод foo() объекта A::first, который еще не был создан.

Из этого примера видно, что ни одна из реализаций, приведенных в статье описанной проблемой не обладает — порядок вызова конструкторов в обоих случаях полностью определяется порядком вызовов методов instance. Я не видел реализации Синглтона, обладающей заявленным недостатком (в реализации Гаммы [3] порядок вызовов конструкторов тоже однозначно определен, однако он пишет о проблеме).

Границы единиц трансляции не при чем, но все таки зависимости между Одиночками ничего хорошего не дадут — они сильно запутывают отладку.

Как ни крути, а Singleton — глобальный объект, со всеми вытекающими проблемами. Если в программе есть ошибка и она зависит от глобального объекта, состояние которого мог менять кто угодно, то найти ее гораздо сложнее (она может повторяться не всегда). Если же мы начнем писать юнит-тесты, то они будут зависеть друг от друга, т.к. будут использовать один глобальный объект, но это не правильно.

Более подробно остановимся на проблеме нарушения принципа единой обязанности (SRP), согласно которому каждый объект должен иметь только одну обязанность, а все его методы должны быть направленны на ее выполнение. Если мы реализуем Singleton так, как показано выше, то наш класс помимо своей основной работы начинает контролировать количество созданных экземпляров. Это плохо, но можно решить проблему, например при помощи шаблонов:

template <class T>
class Singleton {
public:
  static T& instance() {
    static T instance;
    return instance;
  }

private:
  Singleton();
  ~Singleton();
  Singleton(const Singleton &);
  Singleton& operator=(const Singleton &);
};

Вся работа (обязанность — по принципу SRP) по контролю того, что создан единственный экземпляр объекта, возложена на класс Singleton. Если в какой-либо точке программы нам потребовался экземпляр (единственный) класса SimpleClass, достаточно вызвать Singleton<SimpleClass>::instance(). Если мы захотим запретить создание других экземпляров класса SimpleClass (переместим конструктор в секцию private), то Singleton должен стать дружественным классом для SimpleClass:

class SimpleClass {
  friend class Singleton<SimpleClass>;
// ...
};

Таким образом, большинство проблем, связанных с использованием шаблона Singleton, можно избежать, однако нельзя устранить проблемы, связанные с тем, что он является глобальным объектом — проблемы отладки и тестирования. Однако, глобальность объекта была нашей целью, не удивительно что мы ее и добились.

Надо помнить, что в большинстве случаев этого шаблона все-таки возможно избежать, и следует это сделать, т.к. удалить его из проекта будет довольно сложно. Одиночка — очень заманчивый шаблон, но стоит относиться к нему с осторожностью.

Singleton — пример использования на С++, Qt

Singleton-example-UML

Пример использования Singleton. UML диаграмма

На диаграмме показано, что в приложении есть:

  • регулятор громкости — VolumeController, встроенный в меню. Регулятор является элементом управления (виджетом);
  • элементы издающие звук — SoundItem, их может быть много, они могут быть разбросаны по всему приложению, но каждое из них должно реагировать на изменение громкости;
  • класс громкости — Volume, состояние которого изменяется под действием VolumeController, при каждом изменении генерируется сигнал, который должны обрабатывать элементы SoundItem;
  • менеджер базы данных настроек — SettingsDB, при каждом изменении громкости обновляется соответствующая запись в базе. При загрузке программы база устанавливает громкость (как-то влияет на экземпляр класса Volume).

Для того, чтобы объект Volume был глобальным и из любой точки программы к нему можно было обратиться, можно использовать Singleton. Я выбрал реализацию Одиночки в виде шаблонного класса.

class Volume : public QObject {
  Q_OBJECT
private:
  explicit Volume(QObject *parent = 0);
signals:
  void changed(int volume);
public slots:
  int get() const;
  void set(int volume);
private:
  int m_volume;

  friend class Singleton<Volume>;
};

#define VOLUME Singleton<Volume>::instance()

Макросом VOLUME я заменил длинную строку Singleton<Volume>::instance(). Класс Volume очень прост — он не знает о существовании каких либо других объектов в программе, он лишь позволяет получить и установить громкость и генерирует сигналы об ее изменении.

VolumeController::VolumeController(QWidget *parent) :
  QWidget(parent),
  ui(new Ui::VolumeController) {
  ui->setupUi(this);

  ui->volume->setSliderPosition(VOLUME.get());
  connect(ui->volume, SIGNAL(sliderMoved(int)), &VOLUME, SLOT(set(int)));
  connect(&VOLUME, SIGNAL(changed(int)), ui->volume, SLOT(setValue(int)));
}

Регулятор громкости связывает сигнал изменения положения ползунка громкости со слотом установки объекта Volume, а также сигнал изменения объекта Volume со слотом ползунка.

Actor::Actor(QWidget *parent): Block(parent) {
  QMediaPlaylist *playlist = new QMediaPlaylist(&m_mediaPlayer);

  m_mediaPlayer.setPlaylist(playlist);
  playlist->addMedia(QUrl("qrc:/game/audio/tukran.ogg"));
  playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);

  connect(&VOLUME, SIGNAL(changed(int)), &m_mediaPlayer, SLOT(setVolume(int)));
  m_mediaPlayer.setVolume(VOLUME.get());
// ...
}

Элементы, издающие звуки, связывают сигнал изменения громкости с соответствующим слотом объекта QMediaPlayer (отвечающего за воспроизведение звука).

SettingsDB::SettingsDB(QObject *parent) :
  DBFacade("settings.sqlite", parent) {
// ...
  connect(&VOLUME, SIGNAL(changed(int)), SLOT(on_volumeChanged(int)));
}
void SettingsDB::loadFromDB() {
  exec(tr("SELECT value FROM settings WHERE property = ") + qs("volume"));
  m_query->first();
  VOLUME.set(m_query->value(0).toInt());
}

void SettingsDB::on_volumeChanged(int volume) {
  exec(
    tr("UPDATE settings SET value = ") + QString::number(volume) +
    " WHERE property = " + qs("volume")
  );
}

Менеджер, при загрузке базы явно вызывает метод set объекта Volume для установки начальной громкости, а при изменении громкости вносит изменения в базу данных.

Таким образом шаблон проектирования Singleton позволил соединить друг с другом элементы, разбросанные по всей программе за счет предоставления глобальной точки доступа к объекту Volume.

Как отмечалось выше, Singleton — очень обманчивый паттерн, его часто хочется применить, но делать это надо очень осторожно. В приведенном примере могло появиться желание сделать одиночкой регулятор громкости и избавиться от класса Volume (у него ведь все равно есть слоты для установки и получения значения ползунка регулировки), но это было бы плохим решением — ведь в будущем мы могли бы поместить регулятор громкости на каждый экран программы.

В рассмотренном в статье случае, использование Singleton оправдано, т.к. мы полагаем что громкость будет являться глобальным свойством приложения, однако если мы захотим ввести отдельный регулятор для громкости каких-нибудь уведомлений — появится второй Синглтон, а затем могут появиться зависимости между экземплярами и проблемы.

Ссылки по теме:

  1. Разработка игры на С++, Qt [Электронный ресурс] — режим доступа: https://pro-prof.com/archives/1520. Дата обращения: 05.09.2014.
  2. Шаблон Producer/Consumer [Электронный ресурс] — режим доступа: https://pro-prof.com/archives/1034. Дата обращения: 05.09.2014.
  3. Э. Гамма Приемы объектно-ориентированного проектирования. Паттерны проектирования / Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес. – СПб.: Питер, 2009. – 366 с.
  4. Мартин Р. Чистый код. Создание, анализ и рефакторинг. Библиотека программиста. — СПб.: Питер, 2014. — 464 с.
  5. Использование паттерна синглтон [Электронный ресурс] — режим доступа: http://habrahabr.ru/post/116577/. Дата обращения: 05.09.2014.
  6. Ларман, К. Применение UML и шаблонов проектирования [Текст]: пер. с англ. / К. Ларман; М.: Изадетельский дом «Вильямс», 2004. — 624 с.

 

4 thoughts on “Паттерн Singleton. Описание. Пример использования

  1. Николай

    Очень хорошо написана статья. Хотелось бы побольше таких объяснений. Большое спасибо.

    Reply
    1. admin Post author

      Если понравилась эта статья — посмотрите другие материалы по шаблонам проектирования на блоге. Я всех их пишу в одном стиле и стараюсь придумывать примеры.

      Reply
      1. Николай

        Да. Я видел, просто хочется больше шаблонов 😉 А так действительно все довольно круто.

        Reply
      2. Николай

        Написано очень объёмно, но читалось довольно легко. Примеры понравились, почерпнул немного новых знаний, спасибо за такой материал.

        Reply

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*

code