Одиночка гарантирует, что класс имеет только один экземпляр и предоставляет глобальную точку доступа к этому экземпляру.
- Код должен зависеть от абстракций, а не от конкретных классов
В примере рассмотрена работа шоколадной фабрики. Существует нагреватель, который доводит смесь молока и шоколада до кипения. Данный нагреватель умеет наполняться смесью, доводить ее до кипения, сливать смесь. Структура нагревателя сохраняет его состояния (пустой или нет, кипит смесь или нет). Все методы используют информацию о состоянии нагревателя, чтоб исключить ошибочное выполнение:
- нагреватель наполняется только если он пуст;
- нагревает смесь, если он не пуст, и смесь не кипит;
- сливает смесь, если он не пуст и смесь кипит.
Но если будет создано два или более экземпляров структуры управления нагревателем, то у каждого экземпляра будут свои переменные для хранения состояний. Это непременно приведет к некорректному выполнению методов. Данная проблема решается гарантированным созданием единственного экземпляра структуры нагревателя. В таком случае и применяется паттерн Одиночка.
В go существует несколько способов реализовать данный паттерн:
Во всех случаях используется глобальная переменная для хранения созданного экземпляра.
var instance *chocolateBoiler
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &chocolateBoiler{empty: true}
}
return instance
Не самое удачно решение, если необходима высокая производительность. Данный метод станет "бутылочным горлышком", так как в один момент времени только одна go-рутина сможет получить доступ к экземпляру, остальные будут ждать.
if instance == nil {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &chocolateBoiler{empty: true}
}
}
return instance
Более удачное решение, так как не происходит блокировка при каждом обращении, а только при первом. Дополнительная проверка нужна на случай, если произойдет несколько обращений одновременно, и, как следствие, несколько go-рутин пройдут первую проверку. Дальше go-рутины будут по очереди устанавливать блокировку и снова проверять, был ли уже проинициализирован экземпляр.
var initialized uint32
if atomic.LoadUint32(&initialized) == 0 {
mu.Lock()
defer mu.Unlock()
if initialized == 0 {
instance = &chocolateBoiler{empty: true}
atomic.StoreUint32(&initialized, 1)
}
}
return instance
Аналогично предыдущему способу, но используется более надежный метод проверки был ли экземпляр уже проинициализирован. Из-за оптимизации при компиляции нет никакой уверенности, что проверка экземпляра будет выполняться атомарно, поэтому используется специальный пакет atomic, гарантирующий атомарность при установке и проверки ключа.
once.Do(func() {
instance = &chocolateBoiler{empty: true}
})
return instance
Самый оптимальный вариант. Здесь используется объект Once
из стандартного пакета
sync
. Once
это объект, который позволяет выполнять некоторое действие только
один раз.
type Once struct {
m Mutex
done uint32
}
Do
вызывает функцию f
только в том случае, если это первый вызов Do
для
этого экземпляра Once
. Другими словами, если у нас есть var once Once
и
once.Do(f)
будет вызываться несколько раз, f
выполнится только в
момент первого вызова, даже если f
будет иметь каждый раз другое значение.
Для вызова нескольких функций таким способом нужно несколько
экземпляров Once
.
Do
предназначен для инициализации, которая должна выполняться единожды.
Так как f
ничего не возвращает, может быть необходимым использование
замыкания для передачи параметров в функцию, выполняемую Do
:
config.once.Do(func() { config.init(filename) })
Поскольку ни один вызов к Do
не завершится пока не произойдет
первый вызов f
, то f
может заблокировать последующие вызовы
Do
и получится deadlock.
Если f
паникует, то Do
считает это обычным вызовом и, при последующих
вызовах, Do
не будет вызывать f
.
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 { // Check
return
}
o.m.Lock() // Lock
defer o.m.Unlock()
if o.done == 0 { // Check
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
- Паттерн Одиночка гарантирует, что в приложении существует не более одного экземпляра данного класса.
- Паттерн Одиночка также предоставляет глобальную точку доступа к этому экземпляру.
- Стандартная реализация в ООП-ориентированных языках подразумевает использование приватного конструктора и статического метода в сочетании со статическое переменной (статическим свойством класса).
- Проанализируйте ограничения по производительности и затратам ресурсов, тщательно выберите реализацию Одиночки для многопоточного приложения.
- Будьте внимательны при использовании загрузчиков классов, они могут привезти к созданию нескольких экземпляров, а это противоречит основной цели паттерна.