Перейти к содержимому

88. Singleton (Одиночка)

Singleton — паттерн проектирования, гарантирующий что класс имеет только один экземпляр и предоставляющий глобальную точку доступа к нему. В JavaScript реализуется несколькими способами — от простого объекта до классов с приватным конструктором.

Если объект создаётся один раз — он уже Singleton:

// Простейший синглтон — объект-литерал
const config = {
host: 'localhost',
port: 3000,
debug: false,
get(key) { return this[key] },
set(key, value) { this[key] = value },
}
config.set('debug', true)
console.log(config.get('debug')) // true
// Это один и тот же объект везде в коде
import { config } from './config.js'
class Database {
static #instance = null // приватное статическое поле
#connection = null
constructor(connectionString) {
if (Database.#instance) {
return Database.#instance // возвращаем существующий экземпляр
}
// Первый вызов — инициализируем
this.#connection = { url: connectionString, connected: false }
Database.#instance = this
}
connect() {
this.#connection.connected = true
console.log(`Подключено к ${this.#connection.url}`)
return this
}
query(sql) {
if (!this.#connection.connected) throw new Error('Нет соединения')
console.log(`Запрос: ${sql}`)
return []
}
static getInstance() {
if (!Database.#instance) {
new Database('postgresql://localhost/mydb')
}
return Database.#instance
}
}
const db1 = new Database('postgresql://host1/db')
const db2 = new Database('postgresql://host2/db') // возвращает тот же экземпляр!
console.log(db1 === db2) // true
// Рекомендуется через getInstance
const db = Database.getInstance()
db.connect().query('SELECT * FROM users')
const AppState = (function() {
let instance
function createInstance() {
const state = {
user: null,
theme: 'dark',
language: 'ru',
notifications: [],
}
return {
get(key) { return state[key] },
set(key, value) { state[key] = value; return this },
addNotification(msg) {
state.notifications.push({ msg, time: Date.now() })
return this
},
getNotifications() { return [...state.notifications] },
}
}
return {
getInstance() {
if (!instance) instance = createInstance()
return instance
}
}
})()
const state1 = AppState.getInstance()
const state2 = AppState.getInstance()
state1.set('user', { name: 'Алексей' }).addNotification('Добро пожаловать!')
console.log(state2.get('user').name) // 'Алексей' — тот же объект!
console.log(state1 === state2) // true

Создаётся только при первом обращении:

class ServiceLocator {
static #services = new Map()
static register(name, factory) {
this.#services.set(name, { factory, instance: null })
}
static get(name) {
const service = this.#services.get(name)
if (!service) throw new Error(`Сервис "${name}" не зарегистрирован`)
// Lazy: создаём экземпляр только при первом запросе
if (!service.instance) {
service.instance = service.factory()
}
return service.instance
}
}
// Регистрируем сервисы
ServiceLocator.register('logger', () => ({
log: (msg) => console.log(`[LOG] ${msg}`)
}))
ServiceLocator.register('cache', () => {
const store = new Map()
return {
set: (k, v) => store.set(k, v),
get: (k) => store.get(k),
}
})
// Используем
const logger = ServiceLocator.get('logger')
logger.log('Привет!')
const cache1 = ServiceLocator.get('cache')
const cache2 = ServiceLocator.get('cache')
cache1.set('key', 'value')
console.log(cache2.get('key')) // 'value' — тот же экземпляр
console.log(cache1 === cache2) // true

В ES Modules каждый файл — уже Singleton по умолчанию:

// store.js — выполняется только один раз при первом импорте
let _state = {
count: 0,
user: null,
}
export function getState() { return { ..._state } }
export function setState(updates) { Object.assign(_state, updates) }
export function increment() { _state.count++ }
// Любой файл, который импортирует store.js, получает тот же экземпляр
// import { getState, setState } from './store.js'

✅ Подходит:

  • Конфигурация приложения
  • Пул соединений с БД
  • Logger/система логирования
  • Кэш
  • EventBus / глобальный шина событий

❌ Избегать:

  • Если состояние должно быть независимым (тесты!)
  • Как замена правильной архитектуре
  • Когда создаёт скрытые зависимости

Тестирование: Синглтоны усложняют тестирование. Используйте Dependency Injection или позвольте сбрасывать экземпляр между тестами.