Singleton и Factory в SPA

Применение паттернов Singleton и Factory в контексте SPA: Redux store, Angular DI-сервисы, фабрики UI-компонентов. Канонические описания самих паттернов — см. Singleton Pattern и Factory Pattern.

Singleton гарантирует существование единственного экземпляра класса в приложении; Factory инкапсулирует логику создания объектов, скрывая детали конструирования за единым интерфейсом.

Зачем нужно

  • Singleton решает задачу разделяемого состояния (кэш, конфиг, EventBus) без глобальных переменных
  • Factory устраняет жёсткую привязку кода к конкретным классам — создание объектов становится расширяемым
  • Оба паттерна появляются в реальных библиотеках: Redux store (Singleton), React.createElement / фабрики компонентов (Factory)

Где используется

  • Singleton: Redux store, конфигурация приложения, WebSocket-соединение, логгер
  • Factory: создание UI-компонентов по типу (кнопки, иконки), парсинг данных из API, IoC-контейнеры
  • Angular — сервисы по умолчанию являются Singleton в рамках DI-контейнера
  • Тестирование — Factory позволяет создавать тестовые двойники объектов

Основной контент

Singleton

Классическая реализация на TypeScript:

class AppConfig {
  private static instance: AppConfig | null = null;

  readonly apiUrl: string;
  readonly debug: boolean;

  private constructor {
    this.apiUrl = process.env.API_URL ?? "http://localhost:3000";
    this.debug = process.env.NODE_ENV !== "production";
  }

  static getInstance: AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig();
    }
    return AppConfig.instance;
  }
}

const config1 = AppConfig.getInstance;
const config2 = AppConfig.getInstance;
console.log(config1 === config2); // true — один экземпляр

Модульный Singleton (современный подход в ES-модулях):

// logger.ts — модуль загружается один раз, экспортируемый объект — de facto Singleton
const logs: string = ;

export const logger = {
  log(msg: string) {
    logs.push(`[${new Date.toISOString()}] ${msg}`);
    console.log(msg);
  },
  getLogs {
    return [...logs];
  },
};

// Любой импорт logger в другом файле получит тот же объект

Factory Method

interface Button {
  render: string;
  onClick: void;
}

class PrimaryButton implements Button {
  render { return "<button class='primary'>OK</button>"; }
  onClick { console.log("primary clicked"); }
}

class DangerButton implements Button {
  render { return "<button class='danger'>Delete</button>"; }
  onClick { console.log("danger clicked"); }
}

type ButtonType = "primary" | "danger";

// Factory function
function createButton(type: ButtonType): Button {
  switch (type) {
    case "primary": return new PrimaryButton();
    case "danger": return new DangerButton();
  }
}

const btn = createButton("primary");
btn.render; // "<button class='primary'>OK</button>"

Abstract Factory

interface ThemeFactory {
  createButton: Button;
  createInput: Input;
}

class LightThemeFactory implements ThemeFactory {
  createButton: Button { return new LightButton(); }
  createInput: Input { return new LightInput(); }
}

class DarkThemeFactory implements ThemeFactory {
  createButton: Button { return new DarkButton(); }
  createInput: Input { return new DarkInput(); }
}

// Код не знает о конкретной теме
function buildUI(factory: ThemeFactory) {
  const button = factory.createButton;
  const input = factory.createInput;
  return { button, input };
}

const ui = buildUI(new DarkThemeFactory);

Сравнение паттернов

Singleton:
  Проблема → нужен ровно один экземпляр (store, config)
  Решение  → приватный конструктор + статический getInstance
  Риск     → скрытое глобальное состояние, трудно тестировать

Factory:
  Проблема → код не должен знать, какой конкретный класс создавать
  Решение  → функция/метод, возвращающий объект по параметру
  Риск     → разрастание количества фабрик при большой иерархии классов

Частые ошибки

  • Singleton как замена глобальной переменной — вся программа неявно зависит от одного объекта, тесты мешают друг другу
  • Не сбрасывать Singleton в тестах — состояние из одного теста протекает в следующий
  • Factory без интерфейса — возвращать конкретный класс вместо интерфейса лишает смысла всю абстракцию
  • Singleton в многопоточных средах — в Node.js обычно не проблема, но в Worker Threads каждый worker имеет свой модульный кэш

Связанные темы

Ресурсы