Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью. Шаблон проектирования "Одиночка"(Pattern Singleton) Использование нескольких взаимозависимых одиночек

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

Проблема

Одиночка решает сразу две проблемы, нарушая принцип единственной ответственности класса.

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

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

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


Клиенты могут не подозревать, что работают с одним и тем же объектом.

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

    Но есть и другой нюанс. Неплохо бы хранить в одном месте и код, который решает проблему №1, а также иметь к нему простой и доступный интерфейс.

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

Решение

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

Если у вас есть доступ к классу одиночки, значит, будет доступ и к этому статическому методу. Из какой точки кода вы бы его ни вызвали, он всегда будет отдавать один и тот же объект.

Аналогия из жизни

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

Структура



    Одиночка определяет статический метод getInstance , который возвращает единственный экземпляр своего класса.

    Конструктор одиночки должен быть скрыт от клиентов. Вызов метода getInstance должен стать единственным способом получить объект этого класса.

Псевдокод

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

Этот класс не имеет публичного конструктора, поэтому единственный способ получить его объект - это вызвать метод getInstance . Этот метод сохранит первый созданный объект и будет возвращать его при всех последующих вызовах.

// Класс одиночки определяет статический метод `getInstance`, // который позволяет клиентам повторно использовать одно и то же // подключение к базе данных по всей программе. class Database is // Поле для хранения объекта-одиночки должно быть объявлено // статичным. private static field instance: Database // Конструктор одиночки всегда должен оставаться приватным, // чтобы клиенты не могли самостоятельно создавать // экземпляры этого класса через оператор `new`. private constructor Database() is // Здесь может жить код инициализации подключения к // серверу баз данных. // ... // Основной статический метод одиночки служит альтернативой // конструктору и является точкой доступа к экземпляру этого // класса. public static method getInstance() is if (Database.instance == null) then acquireThreadLock() and then // На всякий случай ещё раз проверим, не был ли // объект создан другим потоком, пока текущий // ждал освобождения блокировки. if (Database.instance == null) then Database.instance = new Database() return Database.instance // Наконец, любой класс одиночки должен иметь какую-то // полезную функциональность, которую клиенты будут // запускать через полученный объект одиночки. public method query(sql) is // Все запросы к базе данных будут проходить через этот // метод. Поэтому имеет смысл поместить сюда какую-то // логику кеширования. // ... class Application is method main() is Database foo = Database.getInstance() foo.query("SELECT ...") // ... Database bar = Database.getInstance() bar.query("SELECT ...") // Переменная "bar" содержит тот же объект, что и // переменная "foo".

Применимость

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

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

Когда вам хочется иметь больше контроля над глобальными переменными.

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

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

Шаги реализации

    Добавьте в класс приватное статическое поле, которое будет содержать одиночный объект.

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

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

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

Для начала вспомним что такое статический класс и для чего он нужен. В любом 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 и т.д. Расскажу сегодня про паттерн проектирования Singleton (одиночка). Цель: создать класс, у которого будет только ОДИН объект. Это значит, что сколько бы раз к нему не обращались, возвращаться будет один и тот же объект, который был создан первый раз. Это удобная вещь и необходимая во многих местах, не зря ее внедряют во фреймворки. Применение:
  • Например необходимо подключить базу данных в проект и класс, который будет отвечать за соединение с ней. Один раз создается соединение и нет нужны создавать его снова и снова
  • Application settings - класс отвечающий за настройки отружения, которые нужны для приложения: хост и порт базы данных и т.д. Они создаются один раз и используются всё время работы приложения.
  • есть еще множество примеров, о которых я не сказал, поэтому пишите в комментариях свои варианты! =)
После этого вступления, как я понимаю можно показать уже пример этого класса: (Хотя я уверен, что каждый из нас сможет придумать реализацию этого) Вот самый простой пример, когда мы ставим приватным конструктор, т.е. нельзя создавать явно объект. И есть статический метод getInstance() , который предоставляет объект. public class Singleton { private static Singleton instance; private Singleton () { } public static Singleton getInstance () { if (instance == null) { instance = new Singleton () ; } return instance; } } Есть проблемы с многопоточностью и тогда можно поставить метод getInstance() маркер synchronized: public class Singleton { private static Singleton instance; private Singleton () { } public static synchronized Singleton getInstance () { if (instance == null) { instance = new Singleton () ; } return instance; } } В конце, как обычно, хочу сказать, что если вы думаете иначе или нашли у меня ошибку - пишите в комментариях! Мы все обсудим, с удовольствием:) Если Вам понравилась статья, пишите "+" и я буду это знать. Это для меня важно:) P.S. Добавляю еще реализации: По мнению Joshua Bloch ’а это лучший способ реализации шаблона Enum Singleton public enum Singleton { INSTANCE; } Double Checked Locking & volatile public class Singleton { private static volatile Singleton instance; public static Singleton getInstance () { Singleton localInstance = instance; if (localInstance == null) { synchronized (Singleton. class ) { localInstance = instance; if (localInstance == null) { instance = localInstance = new Singleton () ; } } } return localInstance; } } И еще On Demand Holder idiom: public class Singleton { public static class SingletonHolder { public static final Singleton HOLDER_INSTANCE = new Singleton () ; } public static Singleton getInstance () { return SingletonHolder. HOLDER_INSTANCE; } } + Ленивая инициализация + Высокая производительность - Невозможно использовать для не статических полей класса Будут вопросы/предложения - пишите в комментарии!

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

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

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

Предположим, что в вашем приложении есть объект, в котором хранятся данные конфигурации приложения. Разумеется, данные конфигурации должны быть доступны разным частям приложения на всех уровнях.

Согласно идеологии ООП, для получения доступа к переменной, например, внутри метода объекта, ее необходимо передать в качестве параметра этого метода или конструктора. Необходимо стараться следовать этому принципу всегда, когда это возможно. Но, в описанной ситуации, трудно представить, через какое количество объектов придется протащить экземпляр класса работы с конфигом.
В PHP есть два способа поместить данные в глобальную область видимости, тем самым, сделать их доступными из любого места программы. Первый – использование суперглобального ассоциативного массива $GLOBALS , содержащего все переменные, объявленные в глобальной области видимости. Второй – использование ключевого слова global , вводящего переменную в текущую область видимости. Какой способ хуже, я затрудняюсь даже предположить. Как бы там ни было, в других языках программирование подобные средства отсутствуют и паттерн Singleton становится единственным способом введения объекта в глобальное пространство.

Класс, реализующий паттерн Singleton становится доступным глобально за счет статического интерфейса. Также к числу его особенностей необходимо отнести блокирование конструктора класса и магических методов __clone() и __wakeup() , описывая их с модификатором доступа private . Это делается для того, чтобы не допустить создание более одного объекта от класса.

newPropetry = "string"; } public static function staticFoo() { return self::getInstance(); } private function __wakeup() { } private function __construct() { } private function __clone() { } } Singleton::getInstance()->Foo(); var_dump(Singleton::staticFoo()); ?>

Единственное свойство класса Singleton::$_instance хранит ссылку на экземпляр, который создается только при первом вызове статического метода Singleton::getInstance() . От повторного создания объекта уберегает условный оператор, с проверкой значения свойства и если ссылка на экземпляр уже существует, она будет возвращена.

Свойство Singleton::$_instance объявлено с модификатором protected , дабы класс можно было наследовать. Нередко паттерн реализуют с сокрытием этого свойства за модификатором private.

Реализацию шаблона Одиночка можно посмотреть, например, в классе CI_Controller из фреймворка CodeIgniter или Zend_Controller_Front из ZendFramework.