Skip to content

Latest commit

 

History

History

singleton

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

Паттерн "Одиночка"

Определение

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

Используемые принципы

  1. Код должен зависеть от абстракций, а не от конкретных классов

Описание примера

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

  • нагреватель наполняется только если он пуст;
  • нагревает смесь, если он не пуст, и смесь не кипит;
  • сливает смесь, если он не пуст и смесь кипит.

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

В go существует несколько способов реализовать данный паттерн:

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

var instance *chocolateBoiler

Простая синхронизация через mutex

mu.Lock()
defer mu.Unlock()

if instance == nil {
    instance = &chocolateBoiler{empty: true}
}

return instance

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

Синхронизация через mutex с двойной проверкой

if instance == nil {
    mu.Lock()
    defer mu.Unlock()

    if instance == nil {
        instance = &chocolateBoiler{empty: true}
    }
}

return instance

Более удачное решение, так как не происходит блокировка при каждом обращении, а только при первом. Дополнительная проверка нужна на случай, если произойдет несколько обращений одновременно, и, как следствие, несколько go-рутин пройдут первую проверку. Дальше go-рутины будут по очереди устанавливать блокировку и снова проверять, был ли уже проинициализирован экземпляр.

Синхронизация через mutex с двойной проверкой и использованием atomic

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, гарантирующий атомарность при установке и проверки ключа.

Нативный способ реализовать паттерн в Go

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()
   }
}

Ключевые моменты

  1. Паттерн Одиночка гарантирует, что в приложении существует не более одного экземпляра данного класса.
  2. Паттерн Одиночка также предоставляет глобальную точку доступа к этому экземпляру.
  3. Стандартная реализация в ООП-ориентированных языках подразумевает использование приватного конструктора и статического метода в сочетании со статическое переменной (статическим свойством класса).
  4. Проанализируйте ограничения по производительности и затратам ресурсов, тщательно выберите реализацию Одиночки для многопоточного приложения.
  5. Будьте внимательны при использовании загрузчиков классов, они могут привезти к созданию нескольких экземпляров, а это противоречит основной цели паттерна.