Как сделать синглтон

Добавил пользователь Дмитрий К.
Обновлено: 05.10.2024

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

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

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

Единый конструктор, который является закрытым (модификатор private) и без параметров. Это предотвратит создание других экземпляров (что было бы нарушением паттерна).

Класс должен быть запечатаннным (модификатор sealed). Строго говоря это является необязательным условием, исходя из вышеизложенных концепций Singleton, но позволяет JIT-компилятору улучшить оптимизацию.

Переменная, которая содержит ссылку на созданный экземпляр, должна быть статической.

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

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

Первая версия - не потокобезопасная

Вышеуказанная реализация не является потокобезопасной. Два разных потока могли бы пройти условие if (source == null), создав два экземпляра, что нарушает принцип Singleton. Обратите внимание, что на самом деле экземпляр, возможно, уже был создан до того, как условие будет пройдено, но модель памяти не гарантирует, что новое значение экземпляра будет видно другим потокам, если не будут приняты соответствующие блокировки.

Вторая версия - простая защита от потоков

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

Обратите внимание, что вместо блокировки типа typeof(Singleton), как это делают в некоторых реализациях Singleton, я блокирую значение статической переменной, которая является закрытой (private) внутри класса. Блокировка объектов, к которым могут обращаться другие классы, ухудшает производительность и вносит риск взаимоблокировки. Я использую простой стиль - по возможности нужно блокировать объекты, специально созданные с целью блокировки. Обычно такие объекты должны быть использовать модификатор private.

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

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

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

Четвертая версия - полностью ленивая загрузка

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

Пятый вариант - с использованием типа Lazy

Это довольная простая реализация, которая хорошо работает. Она также позволяет вам проверить, был ли экземпляр создан с использованием свойства IsValueCreated, если вам это нужно.

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

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

  1. сказал что знает ООП и шаблоны проектирования
  2. сказал, что из всех паттернов знает Singleton

Не хочу поднимать сейчас вопрос о необходимости такого паттерна в JavaScript. Но как это не удивительно, Singleton’ом в JavaScript мы оперируем повседневно.

Обычно я спрашиваю вопрос таким образом:

Какие есть способы получать один и тот же экземпляр объекта?

В результате решил агрегировать все свои знания и оформить в виде сборника решений одной задачи на все случаи жизни. Причем покажу варианты на ES5, ES6+ и TypeScript. TypeScript в данном случае выступает в роли правильного ООП языка. И так, поехали…

Singleton — одиночка

Немного занудства, можно пропустить, если все это знаете.

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

Порождающие шаблоны (англ. Creational patterns) — шаблоны проектирования, которые абстрагируют процесс инстанцирования. Они позволяют сделать систему независимой от способа создания, композиции и представления объектов.

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

Цель

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

Плюсы

  1. Контролируемый доступ к единственному экземпляру.

Минусы

  1. Глобальные объекты могут быть вредны для объектного программирования, в некоторых случаях приводя к созданию немасштабируемого проекта;
  2. Усложняет написание модульных тестов и следование TDD.

Примеры использования

Условия выполнения

Мы воспользуемся подходом TDD и прежде чем писать реализации запишем минимальные тесты, которые должны быть выполнены. Так как это JS и этот язык сильно отличается от более “правильных” языков типа Java, C++ и прочих, в которых создание объекта реализуется через класс, а конструктор не может ничего возвращать, то мы будем так же рассматривать варианты реализации без конструктора и классического определения класса. Ведь в JS это можно делать.

И так, минимальные условия:

Но эти тесты будут модифицированы в процессе разбора решений, так как у нас будут реализации, возвращающие ссылку на объект из конструктора.

Реализации Singleton в TypeScript

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

Все бы хорошо, но в TypeScript 1.8 нет возможности задать приватный конструктор, что приводит к добавлению логики в него. Минусы такой реализации — при первом вызове new Singleton конструктор вернет объект. Да им можно пользоваться, но это нарушает классическую реализацию. Мы можем модифицировать код и получить вот такую версию:

В такой реализации и кода меньше, и нельзя получить экземпляр объекта через new, так как первая иницализация происходит “автоматически” при объявлении класса.

Снова повторюсь: так как у нас необычный язык, то реализовать задачу можно совершенно необычными способами. Например мы можем реализовать одиночку через пространство имен (namespace):

Продолжая развивать тему, мы можем реализовать паттерн через модуль. Я покажу вариант модуля:

Вы так же можете реализовать модуль в отдельном файле:

В такой реализации у нас не то чтобы нет доступа к объекту через вызов new Singleton. У нас вообще нет возможности достучаться до конструктора (ну мы сейчас не рассматриваем цепочку прототипов и прочие возможности JS).

Анонимный класс

Так можНо, если нужно ?

Реализации Singleton в JavaScript

А теперь вернемся к нашему любимому JavaScript со всеми его возможностями. И так, помните я говорил, что мы каждый день пользуемся объектами одиночками? Так как у нас JS , то нам вовсе не нужно создавать класс для получения объекта. Мы можем создать объект, который будет проходить наши тесты:

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

А теперь рассмотрим более сложные примеры на ES5+, позволяющие создавать именно классы, которые будут порождать Singleton.

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

Используем arguments

Метод лаконичен и просто в реализации, но у него есть недостаток. В режиме “use strict” этот код не будет работать, а JSLint/JSHint в (Php|Web)Storm будет показывать ошибку.

Тогда этот же пример можно переписать так:

Скрываем доступ к instance

Пример на ES5 c приватными (локальными) переменными:

В этом примере используем замыкание для реализации.

Краткая запись

ECMAScript 2015

Эпилог

Как видите способов и разнообразия для реализации логики порождающей единственный экземпляр объекта хватает в нашем любимом JavaScript.

Это один из самых простых шаблонов проектирования в Java.

Но когда они спрашивают в глубине концепции синглтона, я в тупик.

Это действительно синглтон, это так сложно?

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

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

Определение так же просто, как 1,2,3 и A, B, C, D.

Давайте посмотрим, как мы можем реализовать Singleton Class.

Как мы можем гарантировать, что объект должен быть только один все время?

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

Логика создания объекта -> что это такое
Как мы создаем объект в Java?

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

Так как же мы можем гарантировать, что конструктор доступен и выполним только один раз?

  1. Запретить доступ конструктора вне класса, чтобы посторонние не смогли создать экземпляр.
    Как это сделать -> как предотвратить доступ к методу вне класса?
    Простой, сделать метод как частное право, аналогично сделать конструктор как частный.
  2. Предотвратите выполнение конструктора внутри класса более одного раза.
    Как это сделать -> это можно реализовать многими способами, давайте рассмотрим это на примере.

Если выше 2 условия выполнены, то у нас всегда будет один объект для нашего класса. И этот класс называется Singleton, так как он создает один объект все время, когда мы запрашиваем.

Не много теории, мы начнем реализовывать это сейчас.

Доступно много способов создания одноэлементного объекта:

Подход 1

  • Стремитесь инициализации или инициализации перед использованием

Экземпляр EagerSingletonClass создается при запуске класса. Поскольку он статический, он загружается и создается во время загрузки EagerSingletonClass.

  • Тестовый класс Junit для вышеуказанного класса для тестирования синглтона.

Преимущество:
Эта стратегия создает объект во время загрузки класса и, следовательно, быстрее и безопаснее сценария многопоточности. Единственное, что мы должны сделать экземпляром — как volatile для обработки многопоточного сценария.

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

Когда использовать вышеуказанную стратегию?
Всякий раз, когда мы на 100% уверены, что этот объект определенно используется в нашем приложении.
ИЛИ ЖЕ
Когда объект не тяжелый, то все в порядке, мы можем управлять скоростью и памятью.

Подход 2

  • Ленивая инициализация или инициализация, как и когда нам нужно

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

В приведенной выше программе мы создали объект только при наличии запроса через метод getInstance ().

Здесь во время первого вызова getInstance () объект ‘singletonInstance’ будет иметь значение null, и он выполняет блок условия if, когда он становится истинным, и создает объект.

Затем последующие вызовы метода getInstance () вернут тот же объект.

Давайте проведем юнит-тестирование

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

Подход 3

  • Синглтон используя Inner class

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

Код модульного теста

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

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

Так что это также обеспечит создание объекта как и когда это потребуется. И это очень просто реализовать. Это также безопасно от многопоточности.

Подход 4

  • Синглтон с сериализацией и де сериализацией

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

Давайте попробуем понять эту проблему программой:

Первое -> сделать сериализуемый класс singleton пригодным для сериализации и десериализации объекта этого класса.
Второе -> записать объект в файл (сериализация)
Третье — изменить состояние объекта
Четвертая вещь -> де сериализация объекта

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

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

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

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

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

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

UML-диаграмма классов паттерна Singleton

UML-диаграмма классов паттерна Singleton

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

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

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

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

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

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

Singleton Мэйерса

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

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

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

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

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

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

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

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

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

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

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

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

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

Читайте также: