Skip to content

Latest commit

 

History

History
219 lines (147 loc) · 12.5 KB

singleton.md

File metadata and controls

219 lines (147 loc) · 12.5 KB

Паттерн Singleton

Введение

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

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

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

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

Реализации

Попытка 0

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)