Использование паттерна синглтон. Шаблон проектирования "Одиночка"(Pattern Singleton) Что такое статический класс

Паттерн Singleton появился, пожалуй, как только появились статичные объекты. В Smalltalk-80 так был сделан ChangeSet, а чуть в самых разных библиотеках стали появляться сессии, статусы и тому подобные объекты, которых объединяло одно - они должны были быть одни-единственные на всю программу.

В 1994 году вышла известная книга «Паттерны проектирования», представив публике, среди 22-х прочих, и нашего героя, которого теперь назвали Singleton. Была там и его реализация на C++, вот такая:

//.h class Singleton { public: static Singleton* Instance(); protected: Singleton(); private: static Singleton* _instance; } //.cpp Singleton* Singleton::_instance = 0; Singleton* Singleton::Instance() { if(_instance == 0){ _instance = new Singleton; } return _instance; }
Что касается потоков, авторы про них даже и не пишут, считая эту проблему малоактуальной. Зато много внимания уделили всяким тонкостям наследования таких классов друг от друга.

Ничего удивительного - на дворе был 1995 год и многозадачные операционные системы были слишком медленными, чтобы кого-то смутить.

В любом случае, этот код не стареет. Используйте подобную реализацию всегда, если класс, который вы хотите объявить, заведомо не будет вызываться из нескольких потоков.

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

Именно в этой книге появился каноничный синглтон Майерса и я не вижу причин, чтобы не привести его здесь:
class singleton { public: static singleton* instance() { static singleton inst; return &inst; } private: singleton() {} };

Аккуратно, лаконично и умело обыгран стандарт языка. Локальная статичная переменная в функции будет вызвана тогда и только тогда, когда будет вызвана сама функция.

Потом его расширили, запретив чуть больше операций:

Class CMySingleton { public: static CMySingleton& Instance() { static CMySingleton singleton; return singleton; } // Other non-static member functions private: CMySingleton() {} // Private constructor ~CMySingleton() {} CMySingleton(const CMySingleton&); // Prevent copy-construction CMySingleton& operator=(const CMySingleton&); // Prevent assignment };
Согласно новому стандарту C++11, больше для поддержки потоков ничего и не надо. Но до полной его поддержки всеми компиляторами надо ещё дожить.

А пока вот уже не меньше полутора десятков лет лучшие умы пытались поймать многопоточный singleton в клетку языкового синтаксиса. C++ не поддерживал потоки без сторонних библиотек - так что очень скоро почти под каждую библиотеку с потоками появился свой Singleton, который был «лучше всех прочих». Александреску уделяет им целую главу, отечественные разработчики борются с ним не на жизнь, а на смерть, а некто Андрей Насонов тоже долго экспериментирует и в итоге предлагает… совершенно другое решение.

В 2004 Мейерс и Александреску объединили усилия и описали Singleton с Double-check locking. Идея проста - если синглтон не обнаружен в первом if-е, делаем lock, и уже внутри проверяем ещё раз.

А пока суд да дело, проблема потоково-безопасного Singleton переползла и на прочие C-подобные языки. Сперва - на Java, а затем и на C#. И вот уже Джон Скит предлагает целый набор решений, у каждого из которых есть и плюсы, и минусы. И их же предлагает Microsoft.

Для начала - тот самый вариант с double-check locking. Microsoft советует записывать его вот так:
using System; public sealed class Singleton { private static volatile Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Singleton(); } } return instance; } } }

Скит, однако, считает, что этот код плох. Почему?

Это не работает в Java. Модель памяти Java до версии 1.5 не проверяла, завершилось ли выполнение конструктора прежде, чем присвоить значение. К счастью, это уже не актуально - давно вышла Java 1.7, а Microsoft рекомендует этот код и гарантирует, что он будет работать.
- Его легко поломать. Запутаешься в скобках - и всё.
- Из-за lock-а он достаточно медлителен
- Есть лучше

Были и варианты без использования потоковых интерфейсков.

В частности, известная реализация через readonly поле. По мнению Скита (и Microsoft), это первый заслуживающий внимания вариант: Вот как он выглядит:

Public sealed class Singleton { private static readonly Singleton instance = new Singleton(); // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Singleton() { } private Singleton() { } public static Singleton Instance { get { return instance; } } }

Этот вариант тоже thread-safe и основан на любопытном свойстве полей readonly - они иницализируются не сразу, а при первом вызове. Замечательная идея, и сам автор рекомендует использовать именно её.

Есть ли у этой реализации недостатки? Разумеется, да:

Если у класса есть статичные методы, то при их вызове readonly поле инициализируется автоматически.
- Конструктор может быть только статичным. Это особенность компилятора - если конструткор не статичен, то тип будет помечен как beforefieldinit и readonly создадутся одновременно со static-ами.
- Статичные конструкторы нескольких связанных Singleton-ов могут нечаянно зациклить друг друга, и тогда уже ничто не поможет и никто не спасёт.

Наконец, известнейшая lazy-реализация с nested-классом.
public sealed class Singleton { private Singleton() { } public static Singleton Instance { get { return Nested.instance; } } private class Nested { // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Nested() { } internal static readonly Singleton instance = new Singleton(); } }

Недостатки у него те же самые, что у любого другого кода, который использует nested-классы.

В последних версиях C# появился класс System.Lazy, который всё это инкупсулирует. А значит, реализация стала ещё короче:
public sealed class Singleton { private static readonly Lazy lazy = new Lazy(() => new Singleton()); public static Singleton Instance { get { return lazy.Value; } } private Singleton() { } }

Легко заметить, что и реализации с readonly, и вариант с nested-классом, и его упрощение в виде lazy объекта не работают с потоками. Вместо этого они используют сами структуры языка, которые «обманывают» интерпретатор. В этом их важнейшее отличие от double-lock"а, который работает именно с потоками.

Почему нехорошо «обманывать» язык? Потому что каждый такой «хак» очень легко нечаянно поломать. И потому что от него нет никакой пользы людям, которые пишут на других языках - а ведь паттерн предполагает универсальность.

Лично я считаю, что проблему потоков надо решать стандартными средствами. В C# встроено множество классов и целые ключевые слова для работы с многопоточностью. Почему бы не использовать стандартные средства, вместо того, чтобы пытаться «обмануть» компилятор.

Как я уже сказал, lock - не лучшее решение. Дело в том, что компилятор разворачивает вот такой lock(obj):

Lock(this) { // other code }

Примерно в такой код:

Boolean lockTaken = false; try { Monitor.Enter(this, ref lockTaken); // other code } finally { if(lockTaken) Monitor.Exit(this); }

Джеффри Рихтер считает этот код весьма неудачным. Во-первых, try - это очень медленно. Во-вторых, если try рухнул, то в коде что-то не то. И когда второй поток начнёт его выполнять, то ошибка скорее всего повторится. Поэтому он призывает использовать для обычных потоков Monitor.Enter / Monitor.Exit, а Singleton переписать на атомарных операциях. Вот так:

Public sealed class Singleton { private static readonly Object s_lock = new Object(); private static Singleton instance = null; private Singleton() { } public static Singleton Instance { get { if(instance != null) return instance; Monitor.Enter(s_lock); Singleton temp = new Singleton(); Interlocked.Exchange(ref instance, temp); Monitor.Exit(s_lock); return instance; } } }

Временная переменная нужна, потому что стандарт C# требует от компилятора сначала создавать переменную, а потом его присваивать. В итоге может получиться так, что в instance уже не null, но инициализация singleton-а ещё не завершена. См. описание подобный случаев в 29-й главе CLR via C# Джеффри Рихтера, раздел The Famous Double-Check Locking Technique .

Таким образом, нашлось место и double-lock-у

Используйте для многопоточных случаев именно этот вариант . Он прост, не делает ничего слабодокументированного, его трудно сломать и он легко переносится на любой язык, где есть атомарные операции.

Статья будет полезна в первую очередь разработчикам, которые теряются на собеседованиях когда слышат вопрос «Назовите основные отличия синглтона от статического класса, и когда следует использовать один, а когда другой?». И безусловно будет полезна для тех разработчиков, которые при слове «паттерн» впадают в уныние или просят прекратить выражаться:)

Что такое статический класс?

Для начала вспомним что такое статический класс и для чего он нужен. В любом CLI-совместимом языке используется следующая парадигма инкапсуляции глобальных переменных: глобальных перменных нет . Все члены, в том числе и статические, могут быть объявлены только в рамках какого-либо класса, а сами классы могут (но не должны ) быть сгруппированы в каком-либо пространстве имен. И если раньше приходилось иммитировать поведение статического класса с помощью закрытого конструктора, то в.NET Framework 2.0 была добавлена поддержка статических классов на уровне платформы. Основное отличие статического класса от обычного, нестатического, в том, что невозможно создать экземпляр этого класса с помощью оператора new . Статические классы по сути являются некой разновидностью простанства имен - только в отличие от последних предназначены для размещения статических переменных и методов а не типов.

Что такое Singleton (Одиночка)?

Один из порождающих паттернов, впервые описанный «бандой четырех» (GoF). Гарантирует, что у класса есть только один экземпляр , и предоставляет к нему глобальную точку доступа . Мы не будем подробно рассматривать здесь этот паттерн, его предназначение и решаемые им задачи - в сети существует масса подробной информации о нем (например и ). Отмечу лишь что синглтоны бывают потокобезопасные и нет, с простой и отложенной инициализацией.

А если нет разницы - зачем плодить больше?

Так в чем же все-таки разница между этими двумя сущностями и когда следует их использовать? Думаю что лучше всего это проиллюстрировать в следующей таблице:
Singleton
Static class
Количество точек доступа
Одна (и только одна) точка доступа - статическое поле Instance
N (зависит от количества публичных членов класса и методов)
Наследование классов
Возможно, но не всегда (об этом - ниже)
Невозможно - статические классы не могут быть экземплярными, поскольку нельзя создавать экземпляры объекты статических классов
Наследование интерфейсов
Возможно, безо всяких ограничений

Возможность передачи в качестве параметров
Возможно, поскольку Singleton предоставляет реальный объект
Отсутствует
Контроль времени жизни объекта
Возможно - например, отложенная инициализация (или создание по требованию )
Невозможно по той же причине, по которой невозможно наследование классов
Использование абстрактной фабрики для создания экземпляра класса
Возможно
Невозможно по причине осутствия самой возможности создания экземпляра
Сериализация
Возможно
Неприменима по причине отсутствия экземпляра

Рассмотрим подробнее перечисленные выше критерии.
Количество точек доступа
Конечно же имеются ввиду внешние точки доступа, другими словами - публичный контракт взаимодействия класса и его клиентов. Это удобнее проиллюстрировать с помощью кода:

Singleton в «канонической» реализации:
public class Session { private static Session _instance; // Реализация паттерна... public static Session Instance { get { // ... return _instance; } } public IUser GetUser() { // ... } public bool IsSessionExpired() { // ... } public Guid SessionID { get { // ... } } }

Статический класс:
public static class Session { // Точка доступа 1 public static IUser GetUser() { // ... } // Точка доступа 2 public static bool IsSessionExpired() { // ... } // ... // Точка доступа N public static Guid SessionID { get { // ... } } }

Наследование классов
С наследованием статических классов все просто - оно просто не поддерживается на уровне языка. С Singleton все несколько сложнее. Для удобства использования многие разработчики чаще всего используют следующую реализацию паттерна:
public class Singleton where T: class { private static T _instance; protected Singleton() { } private static T CreateInstance() { ConstructorInfo cInfo = typeof(T).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new Type, new ParameterModifier); return (T)cInfo.Invoke(null); } public static T Instance { get { if (_instance == null) { _instance = CreateInstance(); } return _instance; } } } public class Session: Singleton { public IUser GetUser() { // ... } public bool IsSessionExpired() { // ... } public Guid SessionID { get { // ... } } }
А поскольку множественное наследование в C# и в любом CLI-совместимом языке запрещено - это означает что мы не сможем унаследовать класс Session от любого другого полезного класса. Выходом является делагирование синглтону управления доступом к экземпляру объекта:
public class Session: CoreObject { private Session() { } public static Session Instance { get { return Singleton.Instance; } } }
Наследование интерфейсов
Использование интерфейсов позволяет достичь большей гибкости, увеличить количество повторно используемого кода, повысить тестируемость, и, самое главное - избежать сильной связности объектов. Статические классы не поддерживают наследования в принципе. Синглтон, напротив, наследование интерфейсов поддерживает в полной мере, поскольку это обычный класс. Но вот использовать эту возможность стоит только в том случае, если экземпляр синглтона планируется передавать в качестве входных параметров в смешанных сценариях или транслировать за границу домена. Пример смешанного сценария:
// Этот класс является синглтоном и реализует интерфейс ISession public class Session: CoreObject, ISession { private Session() { } public static Session Instance { get { return Singleton.Instance; } } } // Этот класс не является синглтоном и вообще может быть объявлен и реализован в другой сборке // полностью скрывая детали реализации public class VpnSession: ISession { } public interface ISessionManager { ISession GetSession(Guid sessionID); // Принимает интерфейс ISession, следуя принципам уменьшения связности bool IsSessionExpired(ISession session); }
Возможность передачи в качестве параметров
Для статических классов это не поддерживается - можно передать разве что тип, но в большинстве ситуаций это бесполезно, за исключением случаев применения механизмов отражения (reflection ). Синглтон же по сути является обычным экземпляром объекта:
// ... ISessionManager _sessionManager; // ... bool isExpired = _sessionManager.IsSessionExpired(Session.Instance);
Контроль времени жизни объекта
Время жизни статического класса ограничено временем жизни домена - если мы создали этот домен вручную, то мы косвенно управляем временем жизни всех его статических типов. Временем жизни синглтона мы можем управлять по нашему желанию. Яркий пример - отложенная инициализация:
public class Singleton where T: class { // ... public static T Instance { get { if (_instance == null) { // Создание "по требованию" _instance = CreateInstance(); } return _instance; } } }
Можно также добавить операцию удаления экземпляра синглтона:
public class Singleton where T: class { // ... public static T Instance { // ... } // Очень опасная операция! public void RemoveInstance() { _instance = null; } }
Данная операция является крайне небезопасной, поскольку синглтон может хранить некоторое состояние и поэтому его пересоздание может иметь нежелательные последствия для его клиентов. Если все же необходимость в таком методе возникла (что скорее всего указывает на ошибки проектирования) то нужно постараться свести к минимуму возможное зло от его использования - например сделать его закрытым и вызывать внутри свойства Instance при определенных условиях:
public class Singleton where T: class { // ... public static T Instance { get { if (!IsAlive) { // Удаление по условию RemoveInstance(); } if (_instance == null) { // Создание "по требованию" _instance = CreateInstance(); } return _instance; } } private void RemoveInstance() { _instance = null; } }
Использование абстрактной фабрики для создания экземпляра класса
Статический класс не поддерживает данной возможности ввиду того, что нельзя создать экземпляр статического класса. В случае с синглтоном все выглядит просто:
public interface IAbstractFactory { T Create(); bool IsSupported(); } public class Singleton where T: class { private static T _instance; private static IAbstractFactory _factory; protected Singleton(IAbstractFactory factory) { _factory = factory; } public static T Instance { get { if (_instance == null) { _instance = _factory.Create(); } return _instance; } } } // Вариант с прямым наследованием от синглтона public class Session: Singleton { protected Session() : base(new ConcreteFactory()) { } // ... }
Правда в варианте с аггрегацией синглтона придеться применить не совсем красивое и, немного громоздкое решение:
public class Session: CoreObject, ISession { private class SessionSingleton: Singleton { protected SessionSingleton() : base(new ConcreteFactory2()) { } } private Session() : base(new CoreContext()) { } public static Session Instance { get { return SessionSingleton.Instance; } } // ... }
Сериализация
Сериализация применима только к экземплярам классов. Статический класс не может иметь экзмпляров поэтому сериализовать в данном случае нечего.

Так что же использовать Синглтон или Статический класс?

В любом случае выбор решения зависит от разработчика и от специфики решаемой им задачи. Но, в любом случае, можно сделать следующие выводы:

Использование синглотона оправдано, когда:

  • Необходимо наследование классов или интерфейсов или делегаровать конструирование объектов фабрике
  • Необходимо использование экземпляров класса
  • Необходимо контролировать время жизни объекта (хоть это и очень редкая задача для синглтона)
  • Необходимо сериализовать объект (такая задача гипотетически возможна, но трудно представить себе сценарии использования)
Использование статических классов целесообразно тогда , когда у вас нет необходимости реализовывать ни один из сценариев перечисленных для синглтона. Основное назначение статических классов все-таки в группировке логически схожих методов, констант, полей и свойств. Например: System.Math , System.BitConverter , System.Buffer , System.Convert и т.д.

Начнём с главного: его код синглтона не потокобезопасен ! Разбор:

If(instance != null) return instance; // (*) Monitor.Enter(s_lock); Singleton temp = new Singleton(); Interlocked.Exchange(ref instance, temp); Monitor.Exit(s_lock); return instance;

Пусть поток 1 начинает выполнять приведённый код. instance в этот момент есть null , так что первая проверка проходит, и выполнение доходит до строки (*).

Пусть в этот момент произойдёт переключение контекста (это ведь возможно, правильно?), и начинает выполняться второй поток. Он точно так же проверяет instance , проходит начальную проверку, пробегает через (*), получает lock , создаёт экземпляр синглтона, записывает его в instance , отпускает lock и выходит. Второй поток получает ссылку на instance только что созданного синглтона.

Теперь управление получает первый поток. Он точно так же получает lock , создаёт второй экземпляр синглтона, записывает его в instance , затирая старое значение, отпускает lock и выходит. Первый поток получает ссылку на другой объект.

Катастрофа.

Теперь по мелочам. Разработчики.NET не просто так сделали lock(obj) синонимом Monitor.Enter(obj, ref lockTaken) , а не просто Monitor.Enter(obj) . Вариант с Monitor.Enter(obj) работает неправильно в случае, если возможны исключения. Поэтому замена lock на явный Monitor.Enter - ухудшение.

Ещё большее ухудшение - отказ от try/catch. Без базара, без try/catch быстрее, только код получается неправильный. (Впрочем, если правильный код нам не нужен, без lock "а было бы ещё быстрее.) Если по каким-то причинам конструктор Singleton "а выбросит исключение (а это может быть не только деление на 0, но и TypeLoadException , например), то s_lock останется залоченным навсегда - deadlock.

Конструктор может быть только статичным. Это особенность компилятора - если конструткор не статичен, то тип будет помечен как beforefieldinit и readonly создадутся одновременно со static-ами.

Это фактически неверно, автор судя по всему просто не понял, что именно написал Jon. У синглтона есть нестатический конструктор, просто явный статический конструктор должен присутствовать . Между «статический конструктор должен присутствовать» и «конструктор может быть только статичным» огромная разница.

Правильная, остроумная, каноническая реализация синглтона с вложенным классом удостоилась лишь замечания «Недостатки у него те же самые, что у любого другого кода, который использует nested-классы». Мне неизвестны недостатки вызванные одним лишь наличием вложенных классов. Если мне кто-либо сообщит, готов изменить своё мнение.

Наконец, самое концептуально верное решение с Lazy критикуется за то, что оно «не работает с потоками», а используют структуры языка, которые «обманывают интерпретатор». Здесь неверно вообще всё, кроме союзов и предлогов. Начнём с того, что C# - компилируемый язык, интерпретатора C# не существует. Затем, никакого обмана нет: Lazy работает строго как обещает его документация. То, что его поведение отличается от поведения других классов - не обман и не трюк. При желании то же самое можно заимплементировать самому вручную.

Затем, правильным при реализации синглтона является не многопоточность или прочие технические мелочи, а простой инвариант: как бы синглтон не использовался, всегда гарантировано наличие не более одного экземпляра синглтона на AppDomain. Именно это гарантируется классом Lazy . Используются ли при этом специальные механизмы для поддержки многопоточности или свойства языка, никому не интересно. Главное - чтобы семантика единственного экземпляра была выдержана. Именно это гарантирует класс Lazy , абстрагируя нас от деталей реализации. И именно поэтому такая имплементация - самая правильная.

Вывод: не все статьи на Хабре одинаково полезны.

Одиночка - паттерн, относящийся к группе порождающих паттернов (шиблонов).

Назначение

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

Псевдоним

Английское название Singleton (одиночка) часто калькируется в русскоязычные тексты и паттерн Одиночка называют "Синглтоном". Мы же будем использовать русское название так как оно весьма удачно.

Мотивация

Для некоторых классов важно, чтобы существовал только один экземпляр.
Например:

  1. Хотя в системе может быть много принтеров, но возможен лишь один спулер.
  2. Должны быть только одна файловая система и единственный оконный менеджер.
  3. В цифровом фильтре может находиться только один аналого-цифровой преобразователь (АЦП). Бухгалтерская система обслуживает только одну компанию.

Как гарантировать, что у класса есть единственный экземпляр и что этот экземпляр легко доступен?
Глобальная переменная дает доступ к объекту, но не запрещает инстанцировать класс в нескольких экземплярах.
Более удачное решение достигается в том случае, если сам класс:

  1. контролирует то, что у него есть только один экземпляр
  2. может запретить создание дополнительных экземпляров, перехватывая запросы на создание новых объектов
  3. и он же способен предоставить доступ к своему экземпляру.

Известные применения

Применяется во многих системах, моделирующих реальные объекты.

Родственные паттерны

С помощью паттерна одиночка могут быть реализованы многие паттерны.
Смотри описание.

Назначение паттерна Singleton

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

Паттерн Singleton предоставляет такие возможности.

Описание паттерна Singleton

Архитектура паттерна Singleton основана на идее использования глобальной переменной, имеющей следующие важные свойства:

  1. Такая переменная доступна всегда. Время жизни глобальной переменной - от запуска программы до ее завершения.
  2. Предоставляет глобальный доступ, то есть, такая переменная может быть доступна из любой части программы.

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

Для решения этой проблемы паттерн Singleton возлагает контроль над созданием единственного объекта на сам класс. Доступ к этому объекту осуществляется через статическую функцию-член класса, которая возвращает указатель или ссылку на него. Этот объект будет создан только при первом обращении к методу, а все последующие вызовы просто возвращают его адрес. Для обеспечения уникальности объекта, конструкторы и оператор присваивания объявляются закрытыми.

Паттерн Singleton часто называют усовершенствованной глобальной переменной.

Реализация паттерна Singleton

Классическая реализация Singleton

Рассмотрим наиболее часто встречающуюся реализацию паттерна Singleton.

// Singleton.h class Singleton { private: static Singleton * p_instance; // Конструкторы и оператор присваивания недоступны клиентам Singleton() {} Singleton(const Singleton&); Singleton& operator=(Singleton&); public: static Singleton * getInstance() { if(!p_instance) p_instance = new Singleton(); return p_instance; } }; // Singleton.cpp #include "Singleton.h" Singleton* Singleton::p_instance = 0;

Клиенты запрашивают единственный объект класса через статическую функцию-член getInstance() , которая при первом запросе динамически выделяет память под этот объект и затем возвращает указатель на этот участок памяти. Впоследcтвии клиенты должны сами позаботиться об освобождении памяти при помощи оператора delete .

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

Singleton Мэйерса

// Singleton.h class Singleton { private: Singleton() {} Singleton(const Singleton&); Singleton& operator=(Singleton&); public: static Singleton& getInstance() { static Singleton instance; return instance; } };

Внутри getInstance() используется статический экземпляр нужного класса. Стандарт языка программирования C++ гарантирует автоматическое уничтожение статических объектов при завершении программы. Досрочного уничтожения и не требуется, так как объекты Singleton обычно являются долгоживущими объектами. Статическая функция-член getInstance() возвращает не указатель, а ссылку на этот объект, тем самым, затрудняя возможность ошибочного освобождения памяти клиентами.

Приведенная реализация паттерна Singleton использует так называемую отложенную инициализацию (lazy initialization) объекта, когда объект класса инициализируется не при старте программы, а при первом вызове getInstance() . В данном случае это обеспечивается тем, что статическая переменная instance объявлена внутри функции - члена класса getInstance() , а не как статический член данных этого класса. Отложенную инициализацию, в первую очередь, имеет смысл использовать в тех случаях, когда инициализация объекта представляет собой дорогостоящую операцию и не всегда используется.

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

Улучшенная версия классической реализации Singleton

С учетом всего вышесказанного классическая реализация паттерна Singleton может быть улучшена.

// Singleton.h class Singleton; // опережающее объявление class SingletonDestroyer { private: Singleton* p_instance; public: ~SingletonDestroyer(); void initialize(Singleton* p); }; class Singleton { private: static Singleton* p_instance; static SingletonDestroyer destroyer; protected: Singleton() { } Singleton(const Singleton&); Singleton& operator=(Singleton&); ~Singleton() { } friend class SingletonDestroyer; public: static Singleton& getInstance(); }; // Singleton.cpp #include "Singleton.h" Singleton * Singleton::p_instance = 0; SingletonDestroyer Singleton::destroyer; SingletonDestroyer::~SingletonDestroyer() { delete p_instance; } void SingletonDestroyer::initialize(Singleton* p) { p_instance = p; } Singleton& Singleton::getInstance() { if(!p_instance) { p_instance = new Singleton(); destroyer.initialize(p_instance); } return *p_instance; }

Ключевой особенностью этой реализации является наличие класса SingletonDestroyer , предназначенного для автоматического разрушения объекта Singleton. Класс Singleton имеет статический член SingletonDestroyer , который инициализируется при первом вызове Singleton::getInstance() создаваемым объектом Singleton . При завершении программы этот объект будет автоматически разрушен деструктором SingletonDestroyer (для этого SingletonDestroyer объявлен другом класса Singleton).

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

Использование нескольких взаимозависимых одиночек

До сих пор предполагалось, что в программе используется один одиночка либо несколько несвязанных между собой. При использовании взаимосвязанных одиночек появляются новые вопросы:

  • Как гарантировать, что к моменту использования одного одиночки, экземпляр другого зависимого уже создан?
  • Как обеспечить возможность безопасного использования одного одиночки другим при завершении программы? Другими словами, как гарантировать, что в момент разрушения первого одиночки в его деструкторе еще возможно использование второго зависимого одиночки (то есть второй одиночка к этому моменту еще не разрушен)?

Управлять порядком создания одиночек относительно просто. Следующий код демонстрирует один из возможных методов.

// Singleton.h class Singleton1 { private: Singleton1() { } Singleton1(const Singleton1&); Singleton1& operator=(Singleton1&); public: static Singleton1& getInstance() { static Singleton1 instance; return instance; } }; class Singleton2 { private: Singleton2(Singleton1& instance): s1(instance) { } Singleton2(const Singleton2&); Singleton2& operator=(Singleton2&); Singleton1& s1; public: static Singleton2& getInstance() { static Singleton2 instance(Singleton1::getInstance()); return instance; } }; // main.cpp #include "Singleton.h" int main() { Singleton2& s = Singleton2::getInstance(); return 0; }

Объект Singleton1 гарантированно инициализируется раньше объекта Singleton2 , так как в момент создания объекта Singleton2 происходит вызов Singleton1::getInstance() .

Гораздо сложнее управлять временем жизни одиночек. Существует несколько способов это сделать, каждый из них обладает своими достоинствами и недостатками и заслуживают отдельного рассмотрения. Обсуждение этой непростой темы остается за рамками проекта. Подробную информацию можно найти в .

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

Результаты применения паттерна Singleton

Достоинства паттерна Singleton

  • Класс сам контролирует процесс создания единственного экземпляра.
  • Паттерн легко адаптировать для создания нужного числа экземпляров.
  • Возможность создания объектов классов, производных от Singleton.

Недостатки паттерна Singleton

  • В случае использования нескольких взаимозависимых одиночек их реализация может резко усложниться.