Паттерн Singleton
- это порождающий паттерн, который гарантирует, что у класса может быть только один экземпляр, к которому есть глобальная точка доступа.
Полезен в случаях, когда необходимо гарантированно иметь только один экземпляр, например, logger. Представьте, что у вас несколько потоков и классов, которые используют логирование. В случае, если будет создано несколько объеков, отвечающих за логирование могут возникнуть проблемы: запись может быть в разные файлы, записи могут перезатирать друг друга и т.д.
Отсюда и требование: один экземпляр с глобальной точкой доступа.
Для организации единой точки доступа первое, что приходит в голову - это скрыть конструкторы, а также предоставить статический метод для контроля за созданием объекта (та самая глобальая точка доступа).
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
Минусы этого подхода в том, что мы не используем ленивую инициализацию - мы сразу создаем объект класса. В ситуации, когда создание объекта ресурсоемкая и тяжелая операция, этот подход может вызвать проблемы.
Можно попробовать инициализировать только при вызове метода, по обращению:
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
Однако такой способ - плох, так как он будет правильно работать только без многопоточного доступа.
В ситуации, если у к методу будет обращение из нескольких потоков будут ошибки: пусть первый поток обращается к методу getInstance()
класса, в момент обращения он видит, что instance хранит null
и начинается создание объекта, но в это же время другой поток обращается к getInstance()
, он также видит, что instance - это null
(так как еще не создался объект, он только начал создаваться). И второй поток также инициализирует создание объекта, т.е по объект будет осздан дважды и в ссылке instance
будет последний из созданных. В ситуации, если создание объекта дорого стоит, занимает время - это может быть критично.
Добавив ключевое слово synchronized
для синхронизации можем разрешить проблему:
class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
Однако и тут не все гладко: ведь пригодится нам synchronized
может только на этапе создания объекта, а в итоге использовать синхронизацию будем всегда. Т.е мы всегда будем бороться за монитор - независимо нужен он или нет.
В принципе, synchronized
- не настолько дорогостоящая вещь, но что-то она все-таки да отъедает от производительности, поэтому такая ситуация не радует.
Именно этот пример обычно и встречается в реализациях, например
Как можно улучшить этот пример еще? Самый видимый - это уменьшить гранулярность блока синхронизации. Т.е уменьшить блок синхронизации - занести его в метод.
class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Теперь мы будем бороться за монитор только в случае, если ссылка - null
, т.е при первой инициализации этого класса.
Ну и самое распространенное решение подобной проблемы с многопоточностью - это использование двойной проверки.
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;
}
}
Однако, как писал Джошуа Блох, самый простой пример реализации паттерна, это:
public class BillPughSingleton {
private BillPughSingleton() {}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
Реализация Билла Пью. Используется Holder-класс
.
Ленивая инициализация, нет проблем с производительностью, но не получится вне static
использовать и обрабатывать ошибки будет тяжеловато.
Хорошо!
- Попытки номер два и три - отличный выбор!
- Используйте также
final
дляSingleton
класса по умолчанию и используйте наследования отSingleton
класса только в случае крайней необходимости, так как могут возникнуть проблемы. public enum Car{ DODGE, LAND_ROVER, AUDI }
Мы не можем создать никак новые инстансы класса `Car` и каждый из них существует в единственном экземпляре!
Понятно, что тут отсутствует ленивая инициализация и прочее, но факт остается фактом.
## Минусы
### Singleton и SOLID
Рассмотрим ситуацию, когда мы хотим сделать единую точку работы с подключением к БД: DatabaseConnection.
Вспомним принципы [SOLID](../SOLID.md):
* S — принцип единственной ответственности.
Нарушение в том, что класс отвечает за несколько функций: будет отвечать подключения **и** еще за управление сущностями DatabaseConnection.
* O — принцип открытости/закрытости: объекты должны быть открыты для расширения, но закрыты для изменения.
Нарушение: возвращает только самого себя, а не расширение.
* L — принцип подстановки Барбары Лисков: объекты могут быть заменены экземплярами своих подтипов без изменения использующего их кода.
Это неверно в случае с синглтоном, потому что наличие нескольких разных версий объекта означает, что это уже не синглтон.
* I — принцип разделения интерфейса: много специализированных интерфейсов лучше, чем один универсальный.
Нет нарушений.
* D — принцип инверсии зависимостей: вы должны зависеть только от абстракций, а не от чего-то конкретного.
Нарушение: зависеть можно только от конкретного экземпляра синглтона.
### Тестирование
Любое тестирование синглтона - это проблема.
Вы не добьётесь полной изоляции тестируемого кода. Проблема вызвана даже не самой логикой, которую вы хотите тестировать, а произвольным ограничением инстанцирования, в которое вы её оборачиваете.
Тесты всегда сохраняют состояние посредством синглтона, что может привести к неожиданному поведению там, где ваши тесты зависят от очерёдности запуска, или оставшееся состояние (либо его некорректная очистка между тестами) скроет от вас реальные проблемы.
### Глобальный доступ
По сути, эта глобальная доступность на уровне любого кода множит на ноль принципы `ООП`, в частности - инкапсуляцию.
Отсюда появляются те же проблемы, что и с глобальными переменными.
### Нельзя создать по настоящему настоящий Singleton
Из-за reflection, возможности использования нескольких ClassLoader-ов и других причин, нельзя создать настоящий-настоящий синглтон.
Могут быть проблем также и в ситуациях, когда вы сериализуете/десериализуете синглтон.
## Применение
Несмотря на то, что паттерн многие перевели уже в антипаттерн, он до сих пор находит свое применение.
По сути - это глобальный объект, с контролем доступа.
Однако из-за минусов дважды взвесьте все за и против.
## Заключение
Паттерн гарантирует наличие единственного экземпляра класса и предоставляет к нему глобальную точку доступа.
Нарушает принципы SOLID, имеет проблемы с многопоточностью и с тестированием.
## Полезные ссылки
1. [Правильный Singleton в Java](https://habr.com/ru/articles/129494/)
2. [Шаблоны Java. Singleton (Одиночка)](https://www.youtube.com/watch?v=vyr9GO7dLBQ)
3. [Реализация шаблона Singleton. Skipy.ru](http://skipy.ru/technics/singleton.html)