Singleton шаблон проектирования. Шаблон проектирования "Одиночка"(Pattern Singleton)

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

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

Для начала вспомним что такое статический класс и для чего он нужен. В любом 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 и т.д.

Последнее обновление: 23.12.2018

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

Когда надо использовать Синглтон? Когда необходимо, чтобы для класса существовал только один экземпляр

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

Классическая реализация данного шаблона проектирования на C# выглядит следующим образом:

Class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; } }

В классе определяется статическая переменная - ссылка на конкретный экземпляр данного объекта и приватный конструктор. В статическом методе getInstance() этот конструктор вызывается для создания объекта, если, конечно, объект отсутствует и равен null.

Для применения паттерна Одиночка создадим небольшую программу. Например, на каждом компьютере можно одномоментно запустить только одну операционную систему. В этом плане операционная система будет реализоваться через паттерн синглтон:

Class Program { static void Main(string args) { Computer comp = new Computer(); comp.Launch("Windows 8.1"); Console.WriteLine(comp.OS.Name); // у нас не получится изменить ОС, так как объект уже создан comp.OS = OS.getInstance("Windows 10"); Console.WriteLine(comp.OS.Name); Console.ReadLine(); } } class Computer { public OS OS { get; set; } public void Launch(string osName) { OS = OS.getInstance(osName); } } class OS { private static OS instance; public string Name { get; private set; } protected OS(string name) { this.Name=name; } public static OS getInstance(string name) { if (instance == null) instance = new OS(name); return instance; } }

Синглтон и многопоточность

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

Static void Main(string args) { (new Thread(() => { Computer comp2 = new Computer(); comp2.OS = OS.getInstance("Windows 10"); Console.WriteLine(comp2.OS.Name); })).Start(); Computer comp = new Computer(); comp.Launch("Windows 8.1"); Console.WriteLine(comp.OS.Name); Console.ReadLine(); }

Здесь запускается дополнительный поток, который получает доступ к синглтону. Параллельно выполняется тот код, который идет запуска потока и кторый также обращается к синглтону. Таким образом, и главный, и дополнительный поток пытаются инициализровать синглтон нужным значением - "Windows 10", либо "Windows 8.1". Какое значение сиглтон получит в итоге, пресказать в данном случае невозможно.

Вывод программы может быть такой:

Windows 8.1 Windows 10

Или такой:

Windows 8.1 Windows 8.1

В итоге мы сталкиваемся с проблемой инициализации синглтона, когда оба потока одновременно обращаются к коду:

If (instance == null) instance = new OS(name);

Чтобы решить эту проблему, перепишем класс синглтона следующим образом:

Class OS { private static OS instance; public string Name { get; private set; } private static object syncRoot = new Object(); protected OS(string name) { this.Name = name; } public static OS getInstance(string name) { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new OS(name); } } return instance; } }

Чтобы избежать одновременного доступа к коду из разных потоков критическая секция заключается в блок lock .

Другие реализации синглтона

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

Потокобезопасная реализация без использования lock

public class Singleton { private static readonly Singleton instance = new Singleton(); public string Date { get; private set; } private Singleton() { Date = System.DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { return instance; } }

Данная реализация также потокобезопасная, то есть мы можем использовать ее в потоках так:

(new Thread(() => { Singleton singleton1 = Singleton.GetInstance(); Console.WriteLine(singleton1.Date); })).Start(); Singleton singleton2 = Singleton.GetInstance(); Console.WriteLine(singleton2.Date);

Lazy-реализация

Определение объекта синглтона в виде статического поля класса открывает нам дорогу к созданию Lazy-реализации паттерна Синглтон, то есть такой реализации, где данные будут инициализироваться только перед непосредственным использованием. Поскольку статические поля инициализируются перед первым доступом к статическому членам класса и перед вызовом статического конструктора (при его наличии). Однако здесь мы можем столкнуться с двумя трудностями.

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

Public class Singleton { private static readonly Singleton instance = new Singleton(); public static string text = "hello"; public string Date { get; private set; } private Singleton() { Console.WriteLine($"Singleton ctor {DateTime.Now.TimeOfDay}"); Date = System.DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { Console.WriteLine($"GetInstance {DateTime.Now.TimeOfDay}"); Thread.Sleep(500); return instance; } } class Program { static void Main(string args) { Console.WriteLine($"Main {DateTime.Now.TimeOfDay}"); Console.WriteLine(Singleton.text); Console.Read(); } }

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

Singleton ctor 16:05:54.1469982 Main 16:05:54.2920316 hello

В данном случае мы видим, что статическое поле instance инициализировано.

Для решения этой проблемы выделим отдельный внутренний класс в рамках класса синглтона:

Public class Singleton { public string Date { get; private set; } public static string text = "hello"; private Singleton() { Console.WriteLine($"Singleton ctor {DateTime.Now.TimeOfDay}"); Date = DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { Console.WriteLine($"GetInstance {DateTime.Now.TimeOfDay}"); return Nested.instance; } private class Nested { internal static readonly Singleton instance = new Singleton(); } } class Program { static void Main(string args) { Console.WriteLine($"Main {DateTime.Now.TimeOfDay}"); Console.WriteLine(Singleton.text); Console.Read(); } }

Теперь статическая переменная, которая представляет объект синглтона, определена во вложенном классе Nested. Чтобы к этой переменной можно было обращаться из класса синглтона, она имеет модификатор internal, в то же время сам класс Nested имеет модификатор private, что позволяет гарантировать, что данный класс будет доступен только из класса Singleton.

Консольный вывод в данном случае мог бы выглядеть следующим образом:

Main 16:11:40.1320873 hello

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

Например, рассмотрим выполнение следующей программы:

Static void Main(string args) { Console.WriteLine($"Main {DateTime.Now.TimeOfDay}"); Console.WriteLine(Singleton.text); Singleton singleton1 = Singleton.GetInstance(); Console.WriteLine(singleton1.Date); Console.Read(); }

Ее возможный консольный вывод:

Main 16:33:33.1404818 hello Singleton ctor 16:33:33.1564802 GetInstance 16:33:33.1574824 16:33:33.1564802

Мы видим, что код метода GetInstance, который идет до вызова конструктора класса Singleton, выполняется после выполнения этого конструктора. Поэтому добавим в выше определенный класс Nested статический конструктор:

Public class Singleton { public string Date { get; private set; } public static string text = "hello"; private Singleton() { Console.WriteLine($"Singleton ctor {DateTime.Now.TimeOfDay}"); Date = DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { Console.WriteLine($"GetInstance {DateTime.Now.TimeOfDay}"); Thread.Sleep(500); return Nested.instance; } private class Nested { static Nested() { } internal static readonly Singleton instance = new Singleton(); } }

Теперь при выполнении той же программы мы получим полноценную Lazy-реализацию:

Main 16:37:18.4108064 hello GetInstance 16:37:18.4208062 Singleton ctor 16:37:18.4218065 16:37:18.4228061

Реализация через класс Lazy

Еще один способ создания синглтона представляет использование класса Lazy:

Public class Singleton { private static readonly Lazy lazy = new Lazy(() => new Singleton()); public string Name { get; private set; } private Singleton() { Name = System.Guid.NewGuid().ToString(); } public static Singleton GetInstance() { return lazy.Value; } }

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

Назначение

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

Псевдоним

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

Мотивация

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

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

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

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

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

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

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

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

Этим постом я открываю цикл статей, посвященных паттернам проектирования. Все написанное мной основывается исключительно на личном опыте.

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

Одним из самых распространенных паттернов является Singleton (Одиночка). Задача этого шаблона ограничить количество экземпляров некоторого класса в пределах приложения. Зачем это может понадобиться на практике? Об этом читайте чуть ниже. План на сегодня таков:

Простая реализация Singleton

Один из самых простых способов реализовать паттерн Singleton на языке Java выглядит так:

Public final class Singleton { private static Singleton _instance = null; private Singleton() {} public static synchronized Singleton getInstance() { if (_instance == null) _instance = new Singleton(); return _instance; } }

Теперь приведу некоторые объяснения по поводу реализации шаблона.

Конструктор класса необходимо объявить с модификатором видимости private. Это предотвратит создание экземпляров класса как с помощью класса Singleton, так и с помощью его наследников. В связи с этим к объявлению класса смело можно дописать модификатор final.

Метод getInstance() создаст ровно один экземпляр класса Singleton. Этот метод объявлен как synchronized. Сделано это вот почему. В многопоточных программах при одновременном вызове метода getInstance() из нескольких потоков можно создать несколько экземпляров класса Singleton. А должен остаться только один!

От модификатора synchronized можно избавиться. Для этого _instance нужно проинициализировать:

Private static final Singleton _instance = new Singleton(),

а в методе getInstance() убрать конструкцию "if". Тогда инициализация произойдет во время загрузки класса.

Но использование поздней инициализации (lazy initialization) предпочтительнее в случае, если создание экземпляра класса занимает много времени. Да и в случае ленивой инициализации есть возможность обработать возникшие исключитальные ситуации при вызове конструктора.

Синглтон с double-checked locking

Проблема с lazy инициализацией остается только в том, что синхронизация нужна по идее только один раз, чтобы несколько потоков не вошли в критическую секцию одновременно. Но после создания экземпляра класса от синхронизации хотелось бы избавиться.

Самый распространенный способ избавиться от лишней синхронизации - это double-checked locking, который выглядит таким образом:

Public final class Singleton { private static volatile Singleton _instance = null; private Singleton() {} public static synchronized Singleton getInstance() { if (_instance == null) synchronized (Singleton.class) { if (_instance == null) _instance = new Singleton(); } return _instance; } }

Кстати, до вресии Java SE 1.5 этот код не работал. Если хотите знать почему - читайте .

Синглтон с Instance Holder

А вот еще один заслуживающий внимания вариант реализации шаблона Одиночка с ленивой инициализацией:

Public final class Singleton { private Singleton() {} private static class Holder { private static final Singleton _instance = new Singleton(); } public static Singleton getInstance() { return Holder._instance; } }

Объект будет проинициализирован при первом вызове метода getInstance(). То есть мы перенесли проблему с синхронизацией на уровень загрузчика классов (class loader).

Но что если в приложении несколько Class Loader"ов или вообще у нас распределенная система с несколькими виртуальнми машинами Java? Тогда все сложно. Поговорим об этом в другой раз.

А вообще сейчас модно говорить, что реально крутой вариант паттерна Одиночка выглядит так:

Public enum Singleton { INSTANCE; }

Вот собственно и все... Ах, да! Зачем нам это нужно.

Практический пример синглтона

Мне приходится чаще всего использовать этот паттерн при работе с конфигурацией. Иногда конфигурацию программы удобно хранить в файле. Допустим, это будет простой текстовый файл "props.txt" со строками типа "ключ=значение". Нам нужно гарантировать, что конфигурация в программе будет в единственном экземпляре. Вторую мы бы и так не создали, но нужно запретить это делать пользователю класса. Итак,

Import java.util.*; import java.io.*; public class Configuration { private static Configuration _instance = null; private Properties props = null; private Configuration() { props = new Properties(); try { FileInputStream fis = new FileInputStream(new File("props.txt")); props.load(fis); } catch (Exception e) { // обработайте ошибку чтения конфигурации } } public synchronized static Configuration getInstance() { if (_instance == null) _instance = new Configuration(); return _instance; } // получить значение свойства по имени public synchronized String getProperty(String key) { String value = null; if (props.containsKey(key)) value = (String) props.get(key); else { // сообщите о том, что свойство не найдено } return value; } }

Теперь для работы с конфигурацией можно использовать конструкцию вида:

String propValue = Configuration.getInstance().getProperty(propKey).

Если имена свойств в "props.txt" меняться не будут, можно описать их в классе таким образом:

Public static final String PROP_KEY = "propKey",

а значения получать так:

String propValue = Configuration.getInstance() .getProperty(Configuration.PROP_KEY).

Паттерн Singleton полезен не только при работе с конфигурациями. Его можно использовать также при написании ConnectionPool, Factory и других вещей.

Вот теперь точно все.

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

Паттерн - пожалуй, самый известный паттерн проектирования. Тем не менее, он не лишен недостатков, поэтому некоторые программисты (например, Егор Бугаенко) считают его антипаттерном. Разбираемся в том, какие же подводные камни таятся в Singleton’е.

Определение паттерна

Само описание паттерна достаточно простое - класс должен гарантированно иметь лишь один объект, и к этому объекту должен быть предоставлен глобальный доступ. Скорее всего, причина его популярности как раз и кроется в этой простоте - всего лишь один класс, ничего сложного. Это, наверное, самый простой для изучения и реализации паттерн. Если вы встретите человека, который только что узнал о существовании паттернов проектирования, можете быть уверены, что он уже знает про Singleton. Проблема заключается в том, что когда из инструментов у вас есть только молоток, всё вокруг выглядит как гвозди. Из-за этого “Одиночкой” часто злоупотребляют.

Простейшая реализация

Как уже говорилось выше, в этом нет ничего сложного:

  • Сделайте конструктор класса приватным, чтобы не было возможности создать экземпляр класса извне.
  • Храните экземпляр класса в private static поле.
  • Предоставьте метод, который будет давать доступ к этому объекту.
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } }

Принцип единственной обязанности

В объектно-ориентированном программировании существует правило хорошего тона - “Принцип едиственной обязанности” (Single Responsibility Principle, первая буква в аббревиатуре SOLID). Согласно этому правилу, каждый класс должен отвечать лишь за один какой-то аспект. Совершенно очевидно, что любой Singleton-класс отвечает сразу за две вещи: за то, что класс имеет лишь один объект, и за реализацию того, для чего этот класс вообще был создан.

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

Тестирование

Один из главных минусов паттерна “Одиночка” - он сильно затрудняет юнит-тестирование. “Одиночка” привносит в программу глобальное состояние, поэтому вы не можете просто взять и изолировать классы, которые полагаются на Singelton. Поэтому, если вы хотите протестировать какой-то класс, то вы обязаны вместе с ним тестировать и Singleton, но это ещё полбеды. Состояние “Одиночки” может меняться, что порождает следующие проблемы:

  • Порядок тестов теперь имеет значение;
  • Тесты могут иметь нежелательные сторонние эффекты, порождённые Singleton’ом;
  • Вы не можете запускать несколько тестов параллельно;
  • Несколько вызовов одного и того же теста могут приводить к разным результатам.

На эту тему есть отличный доклад с “Google Tech Talks”:

Скрытые зависимости

Обычно, если классу нужно что-то для работы, это сразу понятно из его методов и конструкторов. Когда очевидно, какие зависимости есть у класса, гораздо проще их предоставить. Более того, в таком случае вы можете использовать вместо реально необходимых зависимостей заглушки для тестирования. Если же класс использует Singleton, это может быть совершенно не очевидно. Всё становится гораздо хуже, если экземпляру класса для работы необходима определённая инициализация (например, вызов метода init(...) или вроде того). Ещё хуже, если у вас существует несколько Singleton’ов, которые должны быть созданы и инициализированы в определённом порядке.

Загрузчик класса

Если говорить о Java, то обеспечение существования лишь одного экземпляра класса, которое так необходимо для Singleton, становится всё сложнее. Проблема в том, что классическая реализация не проверяет, существует ли один экземпляр на JVM, он лишь удостоверяется, что существует один экземпляр на classloader. Если вы пишете небольшое клиентское приложение, в котором используется лишь один classloader, то никаких проблем не возникнет. Однако если вы используете несколько загрузчиков класса или ваше приложение должно работать на сервере (где может быть запущено несколько экземпляров приложения в разных загрузчиках классов), то всё становится очень печально.

Десериализация

Ещё один интересный момент заключается в том, что на самом деле стандартная реализация Singleton не запрещает создавать новые объекты. Она запрещает создавать новые объекты через конструктор . А ведь существуют и другие способы создать экземпляр класса, и один из них - сериализация и десериализация. Полной защиты от намеренного создания второго экземпляра Singelton’а можно добиться только с помощью использования enum’а с единственным состоянием, но это - неоправданное злоупотребление возможностями языка, ведь очевидно, что enum был придуман не для этого.

Потоконебезопасность

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

Public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }

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

  • Первый поток обращается к getInstance() , когда объект ещё не создан;
  • В это время второй тоже обращается к этому методу, пока первый ещё не успел создать объект, и сам создаёт его;
  • Первый поток создаёт ещё один, второй, экземпляр класса.

Разумеется, можно просто пометить метод как synchronised , и эта проблема исчезнет. Проблема заключается в том, что, сохраняя время на старте программы, мы теперь будем терять его каждый раз при обращении к Singleton’у из-за того, что метод синхронизирован, а это очень дорого, если к экземпляру приходится часто обращаться. А ведь единственный раз, когда свойство synchronised действительно требуется - первое обращение к методу.

Есть два способа решить эту проблему. Первый - пометить как synchronised не весь метод, а только блок, где создаётся объект:

Public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }

Не забывайте, что это нельзя использовать в версии Java ниже, чем 1.5, потому что там используется иная модель памяти. Также не забудьте пометить поле instance как volatile .

Второй путь - использовать паттерн “Lazy Initialization Holder”. Это решение основано на том, что вложенные классы не инициализируются до первого их использования (как раз то, что нам нужно):

Public class Singleton { private Singleton() { } public static Singleton getInstance() { return SingletonHolder.instance; } private static class SingletonHolder { private static final Singleton instance = new Singleton(); } }

Рефлексия

Мы запрещаем создавать несколько экземпляров класса, помечая конструктор приватным. Тем не менее, используя рефлексию, можно без особого труда изменить видимость конструктора с private на public прямо во время исполнения:

Class clazz = Singleton.class; Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true);

Конечно, если вы используете Singleton только в своём приложении, переживать не о чем. А вот если вы разрабатываете модуль, который затем будет использоваться в сторонних приложениях, то из-за этого могут возникнуть проблемы. Какие именно, зависит от того, что делает ваш “Одиночка” - это могут быть как и риски, связанные с безопасностью, так и просто непредсказуемое поведение модуля.

Заключение

Несмотря на то, что паттерн Singleton очень известный и популярный, у него есть множество серьёзных недостатков. Чем дальше, тем больше этих недостатков выявляется, и оригинальные паттерны из книги GOF “Design Patterns” часто сегодня считаются антипаттернами. Тем не менее, сама идея иметь лишь один объект на класс по-прежнему имеет смысл, но достаточно сложно реализовать ее правильно.

Loading...Loading...