Олег Марков
Глобальное состояние в приложениях - global state
Введение
Глобальное состояние (global state) — это данные, к которым может обратиться значительная часть приложения напрямую, без явной передачи через аргументы функций или параметры компонентов.
Проще говоря, если у вас есть переменная или объект, к которому «дотягиваются» отовсюду, — это и есть глобальное состояние.
Глобальное состояние может жить:
- В глобальных переменных языка.
- В синглтонах.
- В централизованных стор-объектах (Redux store, Vuex store, глобальный Context в React).
- В статических полях классов (в ООП-языках).
С одной стороны, глобальное состояние удобно: вы один раз где-то что-то сохранили — и можете использовать это в любом месте приложения. С другой — из-за него растет связанность кода, усложняются тесты и отладка, появляются скрытые зависимости.
В этой статье вы увидите:
- Что именно считается глобальным состоянием и чем оно отличается от локального.
- Какие проблемы возникают при чрезмерном использовании global state.
- В каких случаях глобальное состояние действительно оправдано.
- Какие есть шаблоны и инструменты для управления глобальным состоянием в разных типах приложений.
- Как уменьшить вред глобального состояния, если без него не обойтись.
Я буду периодически показывать вам код на JavaScript/TypeScript и немного на Go и других языках, чтобы вы могли увидеть общие принципы, а не привязку к конкретному стеку.
Что такое глобальное состояние
Локальное vs глобальное состояние
Сначала давайте четко разделим, о чем речь.
Локальное состояние:
- Живет внутри функции, метода, компонента или модуля.
- Доступно только в ограниченной области видимости.
- Передается дальше только явно — через параметры и возвращаемые значения.
- Пример:
let count = 0внутри функции,useStateв отдельном React-компоненте, локальная переменная в Go-функции.
Глобальное состояние:
- Доступно из многих (часто любых) частей программы.
- Как правило, не передается явно — достаточно просто «импортировать» или обратиться к нему по имени.
- Меняется из разных мест, часто в разное время.
- Пример: глобальный Redux store, синглтон
Config, пакетный уровень переменных в Go.
Смотрите, как это выглядит в простом примере на JavaScript.
// Глобальное состояние приложения
// Здесь мы объявляем объект, который может изменить любой модуль
const appState = {
user: null,
theme: 'light',
};
// Любой модуль может прочитать и изменить это состояние
function login(username) {
// Здесь мы мутируем глобальный объект состояния
appState.user = { name: username };
}
function logout() {
// Здесь мы тоже меняем глобальное состояние
appState.user = null;
}
function getCurrentUser() {
// Здесь мы читаем глобальное состояние
return appState.user;
}
Здесь appState — типичный пример глобального состояния. Никакие функции не получают его как аргумент, все обращаются к одной и той же переменной.
Для сравнения, вот вариант с локальным состоянием:
// Здесь мы создаем функцию, которая инкапсулирует состояние
function createAppState() {
// Это состояние доступно только внутри замыкания
let user = null;
let theme = 'light';
// Мы возвращаем методы, которые управляют этим состоянием
return {
login(username) {
// Здесь мы изменяем локальное состояние
user = { name: username };
},
logout() {
user = null;
},
getCurrentUser() {
return user;
},
getTheme() {
return theme;
},
setTheme(newTheme) {
theme = newTheme;
},
};
}
// Здесь мы создаем конкретный экземпляр состояния
const appState = createAppState();
// Дальше appState можно передать в другие части системы явно
Обратите внимание, что во втором примере состояние по-прежнему единое, но оно не глобальное в языковом смысле: его нужно передать туда, где оно нужно. Это уже уменьшает связанность.
Формы глобального состояния
Теперь давайте разберем, в каких формах вы чаще всего встретите global state.
Глобальные переменные языка
В Go:
package config
// Здесь мы объявляем глобальную переменную на уровне пакета
var AppConfig *Config
type Config struct {
DBURL string
}
// Init загружает конфигурацию один раз
func Init() {
// Здесь мы инициализируем глобальную переменную
AppConfig = &Config{
DBURL: "postgres://localhost:5432/app",
}
}
Любой пакет, который импортирует config, может прочитать config.AppConfig. Это удобно, но сильно связывает код с глобальной точкой доступа.
В JavaScript (Node.js):
// Здесь мы записываем в глобальный объект Node.js
global.cache = new Map(); // Глобальный кэш
function getFromCache(key) {
// Здесь мы обращаемся к глобальному кэшу напрямую
return global.cache.get(key);
}
Синглтоны (Singleton)
Синглтон — это паттерн, который ограничивает создание класса одним экземпляром и предоставляет глобальную точку доступа к нему.
На TypeScript:
// Здесь мы описываем синглтон для хранения настроек
class Settings {
private static instance: Settings | null = null;
private theme: 'light' | 'dark' = 'light';
// Прячем конструктор, чтобы нельзя было создать объект напрямую
private constructor() {}
// Здесь мы даем глобальную точку доступа к единственному экземпляру
static getInstance(): Settings {
if (this.instance === null) {
this.instance = new Settings();
}
return this.instance;
}
// Геттер и сеттер для состояния
getTheme() {
return this.theme;
}
setTheme(theme: 'light' | 'dark') {
this.theme = theme;
}
}
// Использование
// Мы получаем глобальный доступ к одному и тому же объекту
const settings = Settings.getInstance();
settings.setTheme('dark');
Синглтон часто выглядит как «красивый» способ сделать глобальное состояние, но по сути это тот же global state, только обернутый в класс.
Централизованные сторы в фронтенде
В современных фронтенд-фреймворках глобальное состояние — это обычно стор:
- React — Redux, Zustand, MobX, глобальный Context.
- Vue — Vuex, Pinia.
- Angular — NgRx, Akita.
Например, Redux-стор:
// Здесь мы описываем срез глобального состояния
interface RootState {
user: { name: string } | null;
theme: 'light' | 'dark';
}
// Здесь мы создаем Redux store
const store = configureStore<RootState>({
reducer: rootReducer,
});
// Любой компонент, подключенный к store, может читать и изменять это состояние
Технически стор — это тоже глобальное состояние, но с четкими правилами:
- Только через actions.
- Только через редьюсеры.
- Без прямой мутации.
Явное и неявное глобальное состояние
Есть важное различие, о котором вы как разработчик часто будете задумываться.
- Явное глобальное состояние — вы прямо видите, где оно объявлено и как к нему обращаются. Например,
const store = ...в корне приложения,config.AppConfigв Go. - Неявное глобальное состояние — оно живет «где-то внутри», например:
- В базе данных.
- В файловой системе.
- В окружении (ENV-переменные).
- В сессии пользователя.
- В кешах фреймворка.
Даже если у вас нет прямой глобальной переменной, сама по себе внешняя система (БД, Redis, внешнее API) тоже выступает как глобальное состояние приложения. Это важно помнить, когда вы тестируете и отлаживаете код.
Зачем используют глобальное состояние
Глобальное состояние не случайно так популярно. Давайте разберем его сильные стороны.
Централизованные общие данные
Во многих приложениях есть данные, которые логично хранить в одном месте:
- Текущий пользователь и его права.
- Тема оформления и настройки интерфейса.
- Язык локализации.
- Конфигурация приложения (адреса сервисов, фичи-флаги).
- Данные, к которым обращаются многие модули (например, кэш или метаданные).
Здесь глобальное состояние делает код проще:
- Вам не нужно протаскивать одни и те же параметры через десятки уровней вызовов.
- Модули могут быть независимыми, но разделять одни и те же данные.
Смотрите на пример с темой оформления:
// globalTheme.ts
// Здесь мы описываем простое глобальное состояние темы
let currentTheme: 'light' | 'dark' = 'light';
export function getTheme() {
// Чтение значения темы
return currentTheme;
}
export function setTheme(theme: 'light' | 'dark') {
// Изменение глобального состояния
currentTheme = theme;
}
Любой компонент может импортировать getTheme и setTheme и работать с одной темой. Это проще, чем передавать theme во все компоненты отдельно.
Кэширование и производительность
Часто глобальное состояние используют для кэшей и одноразовой инициализации:
- Кэш результатов тяжелых запросов.
- Пул соединений с базой данных.
- Один экземпляр HTTP-клиента.
Пример на Go с пулом соединений:
package db
import "database/sql"
// Здесь мы объявляем глобальный объект соединения с БД
var Conn *sql.DB
// Init открывает соединение один раз
func Init(dsn string) error {
// Здесь мы открываем соединение
c, err := sql.Open("postgres", dsn)
if err != nil {
return err
}
// Сохраняем в глобальную переменную
Conn = c
return nil
}
Теперь любой пакет может использовать db.Conn. Это упрощает жизнь, если вы не хотите везде передавать зависимость на базу явно.
Состояние между запросами и сеансами
В веб-приложениях и мобильных приложениях нужно хранить данные между «экранами» и «страницами»:
- Авторизационный токен.
- Корзина покупок.
- Позиция в навигации.
Здесь глобальное состояние (например, Redux store или Vuex store) облегчает жизнь. Вы можете:
- Подписать компонент на нужный кусок стейта.
- Отправлять действия (actions), не думая, кто их обработает.
- Сохранять часть состояния в localStorage или в IndexedDB.
Проблемы и риски глобального состояния
Теперь давайте честно посмотрим, чем глобальное состояние вам может навредить. Это важно для архитектурных решений.
Скрытые зависимости
Код, который читает и пишет в глобальные переменные, формально не принимает никаких параметров, но при этом зависит от внешних данных.
// userService.ts
// Здесь мы неявно используем глобальный store
import { store } from './store';
export function getCurrentUserName() {
// Зависимость есть, но она не видна в сигнатуре функции
const state = store.getState();
return state.user?.name ?? 'Guest';
}
По сигнатуре getCurrentUserName() кажется, что функция «чистая» — никаких аргументов. Но на самом деле она зависит от глобального состояния store.
Это усложняет:
- Понимание кода (вам нужно знать, откуда берется
store). - Повторное использование функций (без store они не работают).
- Тестирование (вам нужно подменять глобальное состояние в тестах).
Состояние сложно отслеживать
Когда состояние можно менять откуда угодно, становится трудно понять:
- Кто именно изменил значение.
- В каком порядке произошли изменения.
- Почему именно сейчас состояние стало неконсистентным.
На практике это выражается в таких симптомах:
- «Иногда» приложение ведет себя странно.
- Ошибку можно воспроизвести только при специфической последовательности действий.
- В логах не видно очевидной причины изменения.
Проблемы с тестированием
Тесты часто должны выполняться независимо и в произвольном порядке. Если вы используете глобальное состояние, оно может «протекать» между тестами.
Пример хрупкого теста на JavaScript:
// globalState.js
// Здесь мы описываем глобальное состояние
export const state = {
counter: 0,
};
// increment.js
// Здесь мы изменяем глобальное состояние
import { state } from './globalState';
export function increment() {
state.counter += 1;
}
// test.js
import { state } from './globalState';
import { increment } from './increment';
test('increment increases counter', () => {
increment();
// Здесь мы ожидаем, что счетчик равен 1
expect(state.counter).toBe(1);
});
test('increment again', () => {
increment();
// Если тесты запускать в другом порядке, состояние может отличаться
expect(state.counter).toBe(1); // Может упасть, если предыдущий тест уже менял counter
});
Здесь тесты зависят от глобального счетчика. Чтобы это исправить, нужно каждый раз сбрасывать состояние или использовать инкапсулированный стор, который легко переинициализировать.
Многопоточность и гонки данных
В многопоточных и асинхронных средах глобальное состояние особенно опасно.
В Go:
var counter int
func Increment() {
// Здесь мы обращаемся к общей переменной из разных горутин
counter++
}
Если несколько горутин вызовут Increment() одновременно, вы можете получить состояние гонки (data race). Корректное значение счетчика будет неопределенным.
Чтобы это исправить, вам нужно синхронизировать доступ:
import "sync"
// Здесь мы защищаем глобальное состояние мьютексом
var (
counter int
mu sync.Mutex
)
func Increment() {
// Здесь мы блокируем мьютекс
mu.Lock()
// Теперь только одна горутина может изменить counter
counter++
// Освобождаем мьютекс
mu.Unlock()
}
Это пример, как глобальное состояние увеличивает сложность синхронизации.
Усложнение рефакторинга
Представьте, что вы хотите поменять формат хранения настроек или перенести часть логики на другой сервис. Если настройки — глобальное состояние, которое импортируется в сотнях файлов, миграция займет гораздо больше времени.
Глобальный объект, от которого зависит много кода, превращается в «монолит внутри монолита».
Когда глобальное состояние оправдано
При всей критике, полностью избегать глобального состояния не получится. Давайте разберем сценарии, где оно действительно уместно.
Настройки и конфигурация приложения
Здесь глобальное состояние часто удобно и достаточно безопасно, если соблюдать несколько правил:
- Конфигурация иммутабельна (после инициализации ее нельзя изменить).
- Инициализация происходит один раз при старте приложения.
- Конфигурация отделена от бизнес-логики.
Пример на Go:
package config
// Здесь мы объявляем глобальную конфигурацию, но не даем ее изменять после инициализации
type Config struct {
DBURL string
}
// Иммутабельная ссылка на конфиг
var appConfig *Config
// InitConfig вызывается один раз при старте
func InitConfig(dbURL string) {
// Здесь мы задаем конфигурацию только один раз
appConfig = &Config{DBURL: dbURL}
}
// GetConfig возвращает ссылку только для чтения
func GetConfig() *Config {
// Здесь мы просто отдаем ссылку, менять ее нельзя по контракту
return appConfig
}
Здесь вы по сути используете глобальное состояние, но ограничиваете его изменение.
Централизованный стор UI-состояния
Во фронтенде глобальный стор (Redux, Vuex и т.п.) — стандартный способ хранить:
- Состояние авторизации.
- Глобальные фильтры.
- Данные, разделяемые несколькими страницами.
Смотрите, как это может выглядеть в React с использованием Context API как простого глобального стейта.
// AppStateContext.tsx
import React, { createContext, useContext, useState } from 'react';
// Здесь мы описываем форму глобального состояния
type AppState = {
user: { name: string } | null;
setUser: (user: { name: string } | null) => void;
};
// Создаем контекст с дефолтным значением null
const AppStateContext = createContext<AppState | null>(null);
// Провайдер оборачивает приложение и создает глобальное состояние
export function AppStateProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<{ name: string } | null>(null);
return (
// Здесь мы предоставляем состояние через контекст
<AppStateContext.Provider value={{ user, setUser }}>
{children}
</AppStateContext.Provider>
);
}
// Хук для удобного доступа
export function useAppState() {
const ctx = useContext(AppStateContext);
if (!ctx) {
throw new Error('useAppState must be used within AppStateProvider');
}
return ctx;
}
Любой компонент может вызвать useAppState() и работать с общим пользователем. При этом:
- Источник глобального состояния явно виден.
- Доступ ограничен только компонентами внутри
AppStateProvider. - В тестах вы можете подменить провайдер.
Кэш и одноразовая инициализация
Кэширование дорогостоящих операций (например, запросов к API) через глобальное состояние — нормальная практика, особенно если:
- Кэш прозрачен для пользователя.
- Вы контролируете жизненный цикл кэша.
Пример простого кэша в Node.js:
// cache.ts
// Здесь мы описываем глобальный кэш запросов
const cache = new Map<string, unknown>();
export function getFromCache<T>(key: string): T | undefined {
// Чтение значения из кэша
return cache.get(key) as T | undefined;
}
export function setToCache<T>(key: string, value: T): void {
// Запись значения в кэш
cache.set(key, value);
}
Если вы понимаете, что кэш — это глобальное состояние, вы сразу начнете думать:
- Когда его очищать.
- Как избежать утечек памяти.
- Не повлияет ли он на тесты.
Как уменьшить вред от глобального состояния
Теперь давайте посмотрим, как сделать глобальное состояние управляемым, если оно вам нужно.
Делайте состояние максимально локальным
Основной принцип: поднимайте состояние только настолько высоко, насколько нужно, и не выше.
Во фронтенде это часто формулируется как «lift state up». Глобальным должно быть только то, что реально нужно многим частям приложения.
Например, не нужно помещать в глобальный Redux store:
- Состояние одного модального окна, которое используется только в одном месте.
- Локальные формы, которые живут только на одной странице.
Лучше оставить их внутри компонента.
Инкапсулируйте доступ к глобальному состоянию
Вместо того чтобы экспортировать глобальную переменную напрямую, создайте небольшой модуль-обертку с четким API.
Плохой вариант:
// state.ts
// Здесь мы экспортируем объект напрямую
export const state = {
user: null as { name: string } | null,
};
Лучше так:
// userState.ts
// Здесь мы скрываем внутреннюю структуру состояния
let user: { name: string } | null = null;
export function getUser() {
// Чтение пользователя
return user;
}
export function setUser(newUser: { name: string } | null) {
// Изменение пользователя
user = newUser;
}
Так вы:
- Можете поменять внутреннюю структуру, не переписывая весь код.
- Можете добавить валидацию, логи, побочные эффекты при изменении.
- Легче подмените реализацию для тестов.
Используйте Dependency Injection (внедрение зависимостей)
В серверных приложениях (Go, Java, .NET) часто применяют внедрение зависимостей вместо прямого использования глобальных переменных.
Смотрите, как можно уйти от глобального db.Conn в Go.
// db.go
package db
import "database/sql"
// Здесь мы определяем интерфейс для работы с БД
type DB interface {
Query(query string, args ...any) (*sql.Rows, error)
Exec(query string, args ...any) (sql.Result, error)
}
// user_repo.go
package user
import "myapp/db"
type Repository struct {
db db.DB
}
// NewRepository получает зависимость явно
func NewRepository(database db.DB) *Repository {
// Здесь мы сохраняем ссылку на интерфейс БД
return &Repository{db: database}
}
func (r *Repository) GetUser(id int) (User, error) {
// Здесь мы используем переданную зависимость
rows, err := r.db.Query("SELECT id, name FROM users WHERE id = $1", id)
// ...
return User{}, err
}
Теперь:
- Вы можете передать реальное соединение в
main. - В тестах вы можете передать фейковую реализацию
db.DB.
Глобальное состояние (db.Conn) может остаться только в main, а бизнес-логика будет от него независима.
Избегайте мутабельного глобального состояния
Если возможно, делайте глобальное состояние иммутабельным (неизменяемым) или хотя бы изменяемым только через ограниченный API.
В Redux эта идея реализована так:
- Состояние не мутируется напрямую.
- Любое изменение — это новый объект.
- Все изменения происходят через actions.
Смотрите упрощенный пример:
// store.ts
// Здесь мы описываем неизменяемое глобальное состояние
type State = {
counter: number;
};
// Начальное состояние
let state: State = { counter: 0 };
// Здесь мы описываем тип действия
type Action =
| { type: 'increment' }
| { type: 'decrement' };
// Reducer описывает, как из старого состояния и действия получить новое
function reducer(current: State, action: Action): State {
switch (action.type) {
case 'increment':
// Возвращаем новый объект состояния
return { ...current, counter: current.counter + 1 };
case 'decrement':
return { ...current, counter: current.counter - 1 };
default:
return current;
}
}
// dispatch изменяет состояние только через reducer
export function dispatch(action: Action) {
// Здесь мы пересчитываем состояние
state = reducer(state, action);
}
// getState только возвращает текущее состояние
export function getState(): State {
return state;
}
Здесь глобальное состояние все еще есть, но:
- Легче отследить, какие действия привели к каким изменениям.
- Можно логировать последовательность actions.
- Проще откатить состояние назад (time travel).
Отделяйте глобальное состояние от бизнес-логики
Очень полезный подход — держать бизнес-логику максимально чистой и независимой от конкретного способа хранения состояния.
То есть:
- Ваши функции бизнес-логики принимают данные и возвращают результат.
- Глобальное состояние используется только на «границах» приложения: контроллерах, обработчиках HTTP, компонентам UI.
Покажу, как это выглядит схематично:
// domain/calcPrice.ts
// Здесь мы описываем чистую бизнес-функцию
export function calcPrice(base: number, discount: number): number {
// Функция зависит только от аргументов
return base - base * discount;
}
// app/useCases.ts
import { calcPrice } from '../domain/calcPrice';
import { getUser } from '../state/userState';
// Здесь мы связываем глобальное состояние и бизнес-функцию
export function getUserPrice(basePrice: number) {
// Читаем глобальное состояние
const user = getUser();
const discount = user ? 0.1 : 0;
// Здесь мы просто вызываем чистую функцию
return calcPrice(basePrice, discount);
}
В итоге:
- Бизнес-правила (
calcPrice) легко тестировать в отрыве от глобального состояния. - Слой приложения (
getUserPrice) работает как мост между глобальным стейтом и доменной логикой.
Практические примеры управления глобальным состоянием
Теперь давайте соберем несколько более цельных примеров, чтобы вы увидели, как это выглядит в реальном коде.
Пример 1: Мини-стор в чистом JavaScript
Смотрите, как можно сделать простой стор без фреймворков.
// store.js
// Здесь мы реализуем минималистичный глобальный стор
// Внутреннее состояние недоступно напрямую
let state = {
user: null,
theme: 'light',
};
// Список подписчиков
const listeners = [];
// Функция для чтения состояния
export function getState() {
// Здесь мы возвращаем копию, чтобы избежать прямой мутации
return { ...state };
}
// Функция для обновления состояния
export function setState(partial) {
// Здесь мы объединяем старое состояние с новым фрагментом
state = { ...state, ...partial };
// Уведомляем всех подписчиков об изменении
listeners.forEach((listener) => listener(state));
}
// Подписка на изменения
export function subscribe(listener) {
// Здесь мы добавляем слушателя
listeners.push(listener);
// Возвращаем функцию отписки
return () => {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
Использование:
// componentA.js
import { getState, setState, subscribe } from './store.js';
// Чтение состояния один раз
const { theme } = getState();
// Подписка на изменения
const unsubscribe = subscribe((newState) => {
// Здесь можно перерисовать компонент
console.log('New theme', newState.theme);
});
// Изменение состояния
function onToggleTheme() {
// Здесь мы обновляем только поле theme
setState({ theme: 'dark' });
}
Здесь вы видите:
- Состояние глобальное, но модульно инкапсулировано.
- Прямой записи в объект состояния нет — все только через
setState. - Компоненты могут подписываться и отписываться.
Пример 2: Глобальное состояние запроса в React
Очень часто в React-приложении вы храните данные запросов к API в глобальном состоянии, чтобы не дублировать запросы между страницами.
Здесь я покажу упрощенный вариант с Context.
// ApiStateContext.tsx
import React, { createContext, useContext, useState } from 'react';
type User = { id: number; name: string };
type ApiState = {
users: User[]; // Кэш списка пользователей
isLoading: boolean; // Флаг загрузки
error: string | null; // Ошибка
loadUsers: () => Promise<void>;
};
const ApiStateContext = createContext<ApiState | null>(null);
export function ApiStateProvider({ children }: { children: React.ReactNode }) {
// Здесь мы создаем локальное состояние в провайдере,
// но по факту оно станет глобальным для всех дочерних компонентов
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function loadUsers() {
// Здесь мы сохраняем флаг, что началась загрузка
setIsLoading(true);
setError(null);
try {
// Здесь вы бы сделали реальный запрос к API
const response = await fetch('/api/users');
const data = (await response.json()) as User[];
// Обновляем глобальное состояние списка пользователей
setUsers(data);
} catch (e) {
// Сохраняем текст ошибки
setError((e as Error).message);
} finally {
// Снимаем флаг загрузки
setIsLoading(false);
}
}
return (
<ApiStateContext.Provider
value={{ users, isLoading, error, loadUsers }}
>
{children}
</ApiStateContext.Provider>
);
}
// Хук для доступа к глобальному состоянию API
export function useApiState() {
const ctx = useContext(ApiStateContext);
if (!ctx) {
throw new Error('useApiState must be used within ApiStateProvider');
}
return ctx;
}
Использование в компоненте:
// UsersList.tsx
import React, { useEffect } from 'react';
import { useApiState } from './ApiStateContext';
export function UsersList() {
// Здесь мы получаем доступ к глобальному состоянию API
const { users, isLoading, error, loadUsers } = useApiState();
useEffect(() => {
// При первом рендере загружаем пользователей
loadUsers();
}, [loadUsers]);
if (isLoading) {
return <div>Загрузка...</div>;
}
if (error) {
return <div>Ошибка {error}</div>;
}
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
Здесь:
- Состояние
usersглобально для всех компонент внутриApiStateProvider. - Вы не дублируете запросы при переходах между страницами.
- В тестах вы можете подменить провайдер и передать фиктивные данные.
Пример 3: Безопасное глобальное состояние в многопоточном Go
Теперь давайте посмотрим, как аккуратно сделать глобальный кэш в Go.
package cache
import "sync"
// Здесь мы описываем потокобезопасный глобальный кэш
// структура, инкапсулирующая мьютекс и карту
type Cache struct {
mu sync.RWMutex
store map[string]interface{}
}
// Глобальный экземпляр кэша
var globalCache = &Cache{
store: make(map[string]interface{}),
}
// Set сохраняет значение в кэш
func Set(key string, value interface{}) {
// Здесь мы блокируем мьютекс на запись
globalCache.mu.Lock()
defer globalCache.mu.Unlock()
// Обновляем карту
globalCache.store[key] = value
}
// Get получает значение из кэша
func Get(key string) (interface{}, bool) {
// Здесь мы блокируем мьютекс на чтение
globalCache.mu.RLock()
defer globalCache.mu.RUnlock()
// Читаем из карты
val, ok := globalCache.store[key]
return val, ok
}
Использование:
package main
import (
"fmt"
"myapp/cache"
)
func main() {
// Здесь мы записываем значение в глобальный кэш
cache.Set("x", 42)
// А здесь читаем
if v, ok := cache.Get("x"); ok {
fmt.Println(v) // 42
}
}
Здесь:
- Глобальное состояние есть, но оно скрыто в пакете
cache. - Доступ синхронизирован, нет data race.
- В тестах можно заменить
globalCacheна другой экземпляр (через дополнительные настройки).
Заключение
Глобальное состояние — мощный, но опасный инструмент. Оно:
- Упрощает доступ к общим данным.
- Уменьшает количество явной передачи параметров.
- Позволяет реализовать кэш, общие настройки, централизованный стор.
Одновременно с этим global state:
- Увеличивает связанность модулей.
- Создает скрытые зависимости.
- Усложняет тестирование и рефакторинг.
- В многопоточной среде требует аккуратной синхронизации.
Ключевая идея, которую стоит вынести:
- Не нужно полностью запрещать себе глобальное состояние.
- Важно осознанно его применять и ограничивать зону влияния.
Практические рекомендации:
- Делайте состояние локальным по умолчанию, глобальным — только по необходимости.
- Инкапсулируйте глобальное состояние в модулях и давайте к нему доступ через функции.
- Используйте иммутабельность и явные события (actions), где это возможно.
- Используйте dependency injection, чтобы бизнес-логика не зависела от глобальных переменных.
- Отделяйте доменную логику от слоев, где живет глобальное состояние (UI, инфраструктура).
Так вы сохраните удобство global state и при этом снизите архитектурный «долг», который он обычно приносит.
Частозадаваемые технические вопросы по теме и ответы
Как протестировать код, зависящий от глобального состояния
Лучший путь — добавить слой абстракции. Вместо прямого импорта глобальной переменной экспортируйте функции доступа. В тестах подменяйте эти функции на фейки или моки.
Пример:
- В
userState.tsэкспортируетеgetUser. - В тестах используете jest.mock или аналог, чтобы вернуть нужного пользователя.
Как временно изменить глобальное состояние в тесте и потом вернуть обратно
Сделайте вспомогательную функцию-хелпер:
- Сохраняете старое значение глобальной переменной.
- Устанавливаете новое перед тестом.
- В
afterEachилиdefer(в Go) возвращаете старое значение.
Важно не изменять глобальные переменные напрямую в нескольких тестах параллельно.
Как перенести существующую логику с глобальными переменными на dependency injection
Двигайтесь по шагам:
- Выделите интерфейсы для зависимостей (например,
DB,Logger). - Переделайте функции и методы так, чтобы они принимали эти интерфейсы как аргументы или поля структур.
- На уровне
mainили корневого модуля создайте реальные реализации и передайте их вниз.
Глобальные переменные можно оставить только вmain, пока идет миграция.
Как избежать гонок данных при использовании глобального состояния в многопоточной среде
Используйте:
- Мьютексы (
sync.Mutex,sync.RWMutexв Go). - Специализированные типы (atomic-переменные, потокобезопасные коллекции).
Вокруг глобального состояния создайте API, который всегда берет и отпускает блокировку внутри, чтобы вызывающий код не думал о синхронизации.
Как решить проблему «prop drilling» без введения большого количества глобального состояния во фронтенде
Для данных, которые нужны многим компонентам, используйте:
- Context API (в React).
- Provide/Inject (в Vue).
- Локальные стора на уровне фич-модулей.
Не обязательно поднимать все в один глобальный Redux или Vuex. Делите состояние на «области» — по фичам или страницам — и делайте для них отдельные небольшие контексты или сторы.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев