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 имеет свой модульный кэш