Антон Ларичев

Введение
Паттерны проектирования — это проверенные временем решения часто встречающихся задач при разработке программного обеспечения. В JavaScript паттерны особенно важны, потому что язык предоставляет огромную свободу: одну и ту же задачу можно решить десятком способов, и без устоявшихся подходов код быстро превращается в хаос.
В этой статье разберём три фундаментальных паттерна: Singleton, Factory и Observer. Каждый из них решает свою задачу и применяется в реальных проектах — от конфигурационных модулей до систем событий и фреймворков.
Singleton
Singleton гарантирует, что у класса будет только один экземпляр, и предоставляет глобальную точку доступа к нему. Это полезно, когда нужен единый источник истины: конфигурация приложения, подключение к базе данных, кеш или логгер.
class Logger {
constructor() {
// Если экземпляр уже создан, возвращаем его
if (Logger.instance) {
return Logger.instance;
}
this.logs = [];
Logger.instance = this;
}
log(message) {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
console.log(message);
}
getLogs() {
return this.logs;
}
}
const logger1 = new Logger();
const logger2 = new Logger();
console.log(logger1 === logger2); // true — это один и тот же объект
В современном JavaScript Singleton часто реализуют через модули. ES-модули кешируются по умолчанию, поэтому экспортируемый объект автоматически становится синглтоном:
// config.js
const config = {
apiUrl: process.env.API_URL,
timeout: 5000
};
export default config;
Любой импорт этого модуля получит один и тот же объект — без классов и проверок.
Factory
Factory (фабрика) инкапсулирует логику создания объектов. Вместо того чтобы напрямую вызывать new в коде клиента, мы передаём ответственность за создание специальной функции или классу. Это удобно, когда тип создаваемого объекта зависит от входных данных.
class EmailNotification {
send(message) {
console.log(`Email: ${message}`);
}
}
class SmsNotification {
send(message) {
console.log(`SMS: ${message}`);
}
}
class PushNotification {
send(message) {
console.log(`Push: ${message}`);
}
}
// Фабрика решает, какой класс инстанциировать
function createNotification(type) {
switch (type) {
case 'email':
return new EmailNotification();
case 'sms':
return new SmsNotification();
case 'push':
return new PushNotification();
default:
throw new Error(`Неизвестный тип уведомления: ${type}`);
}
}
const notifier = createNotification('email');
notifier.send('Заказ оформлен');
Клиентский код не знает о конкретных классах — он работает через единый интерфейс. Добавить новый канал доставки можно, дописав один класс и одну ветку в фабрику; остальной код останется нетронутым.
Когда применять Factory
Фабрика оправдана, если логика создания нетривиальна: нужно подобрать класс по условию, прочитать конфигурацию, провалидировать аргументы или поднять сложный объект с зависимостями. Если же конструктор простой — просто используйте new.
Observer
Observer описывает зависимость «один ко многим»: один объект (Subject) хранит список подписчиков (Observers) и уведомляет их об изменениях. Это основа любой событийной системы — от DOM-событий до Redux и RxJS.
class EventEmitter {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
off(event, callback) {
const callbacks = this.listeners.get(event);
if (!callbacks) return;
// Удаляем конкретный обработчик, чтобы избежать утечек памяти
this.listeners.set(event, callbacks.filter(cb => cb !== callback));
}
emit(event, data) {
const callbacks = this.listeners.get(event);
if (!callbacks) return;
callbacks.forEach(cb => cb(data));
}
}
const orders = new EventEmitter();
function sendEmail(order) {
console.log(`Письмо для заказа ${order.id}`);
}
orders.on('created', sendEmail);
orders.emit('created', { id: 42 });
orders.off('created', sendEmail);
Observer развязывает компоненты: издатель не знает, кто и как обрабатывает событие. Это упрощает тестирование и позволяет добавлять новых подписчиков, не меняя источник.
Частые ошибки
Singleton превращается в глобальную переменную. Чрезмерное использование синглтонов скрывает зависимости и усложняет тестирование. Если класс берёт данные из глобального синглтона, замокать их в юнит-тесте становится мучительно. Передавайте зависимости явно через конструктор там, где это возможно.
Фабрика без выгоды. Если у вас один класс и одна ветка switch, фабрика не нужна — она лишь добавит уровень абстракции. Применяйте паттерн, когда вариантов создания действительно несколько и они меняются.
Утечки в Observer. Забытые подписчики удерживают ссылки на объекты и мешают сборщику мусора их освободить. Всегда отписывайтесь, когда обработчик больше не нужен — особенно в SPA при размонтировании компонентов.
Синхронные обработчики, которые блокируют издателя. Если один подписчик кидает исключение или зависает, остальные могут не получить событие. Оборачивайте вызовы в try/catch или используйте асинхронную доставку через queueMicrotask.
Заключение
Singleton, Factory и Observer — три базовых паттерна, которые встречаются почти в любом нетривиальном JavaScript-проекте. Singleton даёт единую точку доступа к разделяемому ресурсу, Factory скрывает сложность создания объектов, Observer связывает компоненты через события без жёстких зависимостей.
Главное правило — не применять паттерны ради паттернов. Каждый из них решает конкретную проблему, и если проблемы нет, дополнительная абстракция только усложнит код. Начните с простого решения и вводите паттерн, когда почувствуете боль, которую он лечит.






Комментарии
0