Привет, друзья.
Меня зовут Alex Versus и сегодня с вами посмотрим шаблон Singleton, реализацию на языке Golang.
Какая суть?
Одиночка - относится к порождающим шаблонам. Гарантирует:
что у класса/типа есть только один экземпляр
предоставляет к нему глобальную точку доступа.
Какую задачу решает?
Поговорим про задачу, которую решает шаблон. Одиночка решает сразу две проблемы, нарушая принцип единой ответственности (SRP):
Гарантирует наличие единственного экземпляра объекта. Это полезно для доступа к какому либо общему ресурсу, например к базе данных или при реализации единого механизма изменения свойства, например, уровень звука в эквалайзере.
Представим что у нас есть какой-то объект и через некоторое время вы создаете еще один, но вам хотелось бы получить не новый, а уже созданный объект. Такое поведение невозможно создать с помощью стандартных инструментов, таких как конструктор в объектно-ориентированных языках.Предоставить глобальную точку доступа. Обращаю внимание, что это не просто глобальная переменная, через которую можно достучаться до определенного объекта. Глобальная переменная не защищает вас от перезаписи созданного объекта.
Разработчики часто называют Одиночкой объекты, которые выполняют только одну задачу, озвученную выше. Это ошибочное понимание шаблона.
Какое решение в Golang?
Как решить озвученные задачи в GOlang? Кто знаком с реализацией шаблона в ООП, знают, что нужно скрыть конструктор и объявить публичный статический метод, контролирующий жизненный цикл объекта-одиночки. Статический метод обеспечит доступ к объекту из любого места вашей программы. Реализацию можно посмотреть тут. Сколько бы вы не вызывали данный метод - он всегда вернет один и тот же объект. Диаграмма классов шаблона следующая:
В GOlang нет классов и конструкторов. Но есть типы и структуры. Как нам реализовать метод getInstance()
? Создадим определенный тип singleton
:
// declaration defined type
type singleton struct {
}
Инициализируем переменную с типом singleton
, равную пустому значению nil:
// declare variable
var instance *singleton = nil
Для установки значения в instance
нам нужно воспользоваться методом стандартной библиотеки sync.Once
. Он принимает в аргумент функцию, которая отработает один раз за вызов. А так же мы должны с вами определить тип Sigleton
и определить в нем интерфейс с методами работы со свойствами нашего типа:
// defined type with interface
type Singleton interface {
// here will be methods
}
И функцию возврата объекта:
// Get only one object
func GetInstance() Singleton {
once.Do(func() {
instance = new(singleton)
})
return instance
}
Для проверки реализации мы добавим поле в тип singleton
, которое будем менять с помощью сеттеров и геттеров:
// declaration defined type
type singleton struct {
title string
}
И определим в интерфейсе типа Singleton
методы, позволяющий изменить значение этого свойства:
// defined type with interface
type Singleton interface {
SetTitle(t string)
GetTitle() string
}
// Setter for singleton variable
func (s *singleton) SetTitle(t string) {
s.title = t
}
// Getter singleton variable
func (s *singleton) GetTitle() string {
return s.title
}
Реализуем код для работы со свойством данных объектов.
Теперь нам надо проверить, как работает наш код. Напишем небольшой тест и увидим, что работает все корректно при такой реализации:
package Singleton
import "testing"
func TestGetInstance(t *testing.T) {
var s Singleton
s = GetInstance()
if s == nil {
t.Fatalf("First sigletone is nil")
}
s.SetTitle("First value")
checkTitle := s.GetTitle()
if checkTitle != "First value" {
t.Errorf("First value is not setted")
}
var s2 Singleton
s2 = GetInstance()
if s2 != s {
t.Error("New instance different")
}
s2.SetTitle("New title")
newTitle := s.GetTitle()
if newTitle != "New title" {
t.Errorf("Title different after change")
}
}
Запускаем код:
go test -v -run TestGetInstance
=== RUN TestGetInstance
--- PASS: TestGetInstance (0.00s)
PASS
ok main/Singleton 0.310s
Отлично! Кажется, что все ок, но на самом деле нет. Хочу показать еще один тест, который покажет, какая проблема существует:
package Singleton
import (
"fmt"
"strconv"
"sync"
"testing"
)
func TestSecondGetInstance(t *testing.T) {
s1 := GetInstance()
s2 := GetInstance()
var w sync.WaitGroup
for i := 0; i < 3000; i++ {
j := i
w.Add(1)
go func() {
t := "title_" + strconv.Itoa(j)
s1.SetTitle(t)
w.Done()
}()
w.Add(1)
go func() {
t2 := "title_2_" + strconv.Itoa(j)
s2.SetTitle(t2)
w.Done()
}()
}
fmt.Println(s1.GetTitle())
fmt.Println(s2.GetTitle())
}
На выходе мы увидим следующее:
go test -v -run TestSecondGetInstance
=== RUN TestSecondGetInstance
title_2998
title_2_2999
Запускается цикл на 3000 итераций в которых создается по две горутины, в которых вызывается метод установки свойства. Метод меняет значения свойств каждую итерацию, используя сеттер. После выполнения блока кода мы ожидаем, что значения свойств объектов-одиночек будут одинаковыми, но это не так. Почему такое происходит?
Добавив к команде запуска теста опцию -raсе
мы увидим, что происходит так называемая гонка данных. Такое происходит в Golang, когда две горутины одновременно работают с одной и той же переменной и один из потоков пишет в эту переменную. Это проблема многопоточности.
Есть несколько способов решить проблему. Мы с вами не будем останавливаться подробно на этой теме, сегодня говорим про реализацию шаблона Singleton. Я просто покажу одно из решений. Решение называется взаимным исключением, достигнуть можно с помощью мьютекса. Условно, классический мьютекс можно представить в виде переменной, которая может находиться в двух состояниях: в заблокированном и в незаблокированном. При входе в свою критическую секцию поток вызывает функцию перевода мьютекса в заблокированное состояние, при этом поток блокируется до освобождения мьютекса, если другой поток уже владеет им. В Go его можно реализовать через стандартную библиотеку в которой есть примитивы синхронизации: sync.Mutex
и sync.RWMutex
. Реализация выглядит так:
// declaration defined type
type singleton struct {
title string
sync.RWMutex
}
// Setter for singleton variable
func (s *singleton) SetTitle(t string) {
s.Lock()
defer s.Unlock()
s.title = t
}
// Getter singleton variable
func (s *singleton) GetTitle() string {
s.RLock()
defer s.RUnlock()
return s.title
}
Еще раз запустим второй тест и увидим, что сейчас обе переменные равны:
go test -v -run TestSecondGetInstance
=== RUN TestSecondGetInstance
--- PASS: TestSecondGetInstance (0.00s)
PASS
Вот такая реализация шаблона проектирования Singleton
на Golang
.
В чем преимущества шаблона?
Гарантируем наличие в программе единственного объекта.
Предоставляет к нему глобальную точку доступа.
Реализация отложенной инициализации объекта.
Какие недостатки?
Нарушает принцип единой ответственности для ООП языков. Каждый объект/класс/тип должен иметь только одну ответственность и соблюдение этой ответственности должно быть инкапсулировано, методы должны решать задачи этой ответственности.
Маскирует плохой дизайн кода.
Проблемы мультипоточности для языков типа Golang. Рассмотрели на сегодняшнем уроке один из способов, как решается проблема.
При автоматизированном тестировании требует создания mock-объектов. Такие объекты представляют собой конкретную фиктивную реализацию интерфейса, предназначенную исключительно для тестирования и взаимодействия между объектами. В процедурных языках такая конструкция называется dummy.
Singleton — очень обманчивый паттерн, его часто хочется применить, но делать это надо очень осторожно. Например, если мы хотим создать единый регулятор громкости при разработке программного обеспечения для эквалайзера, мы полагаем, что громкость будет являться глобальным свойством программы. Однако если мы захотим ввести отдельный регулятор для громкости каких-нибудь уведомлений — появится второй Singleton, а затем могут появиться зависимости между экземплярами. Нарушения принципа единой обязанности (SRP), согласно которому каждый объект должен иметь только одну обязанность, а все его методы должны быть направленны на ее выполнение. Если мы реализуем Singleton, то наш тип помимо своей основной работы может начать контролировать количество созданных экземпляров. Singleton — глобальный объект, со всеми вытекающими проблемами.
Если в программе есть ошибка и она зависит от глобального объекта, состояние которого мог менять кто угодно, то найти ее гораздо сложнее. Или другой пример - мы можем начать писать юнит-тесты, которые всегда зависят друг от друга, если используют один глобальный объект.
А на сегодня все. Надеюсь помог вам разобраться в тонкостях реализации практики на языке Golang. Рад был поделиться материалом.
Alex Versus. Всем удачи!