JavaScript и джентльмены: зачем вам паттерны проектирования (и как они упростят жизнь)
Вы когда-нибудь открывали свой старый проект на JavaScript и думали: «Кто это написал? Это же спагетти-код!»? Если да — добро пожаловать в клуб. Рано или поздно каждый разработчик сталкивается с ситуацией, когда код превращается в хаос. И тут на помощь приходят паттерны проектирования в JavaScript — проверенные рецепты для типовых задач.
Но давайте сразу договоримся: паттерны — это не серебряная пуля. Это не магия, которая сделает ваш код идеальным за одну ночь. Это инструменты. А любой инструмент хорош только тогда, когда вы знаете, когда и зачем его применять. Именно об этом мы и поговорим.
Что такое паттерны проектирования простыми словами?
Представьте, что вы — архитектор. Вы не придумываете каждый раз новую форму колеса или дверного проема. Вы используете проверенные решения: арки, колонны, несущие стены. Паттерны проектирования в JS — это те же «архитектурные чертежи», только для кода.
Они решают три главные проблемы:
- Повторяемость — не нужно изобретать велосипед;
- Читаемость — другой разработчик сразу поймет вашу логику;
- Масштабируемость — код легко расширять без риска все сломать.
В этой статье мы разберем четыре самых популярных паттерна, которые реально пригодятся в повседневной работе. Никакой воды — только код и практика.
1. Singleton (Одиночка): когда вам нужен только один
Бывают ситуации, когда в приложении должен существовать ровно один экземпляр класса. Например:
- Глобальное хранилище состояния (как Redux store);
- Логгер для записи ошибок;
- Подключение к базе данных или API.
Паттерн Singleton гарантирует, что сколько бы раз вы ни пытались создать объект — получите один и тот же.
Пример: Счетчик посещений
class Counter {
constructor() {
if (Counter.instance) {
return Counter.instance;
}
this.count = 0;
Counter.instance = this;
}
increment() {
this.count++;
}
getCount() {
return this.count;
}
}
// Использование
const counter1 = new Counter();
const counter2 = new Counter();
counter1.increment();
counter1.increment();
console.log(counter2.getCount()); // 2 — потому что это один и тот же объект
console.log(counter1 === counter2); // true
Когда использовать: когда вам критично иметь единую точку доступа к ресурсу. Но будьте осторожны — частый побочный эффект Singleton в JS — глобальное состояние, которое сложно тестировать. Не злоупотребляйте.
2. Module (Модуль): инкапсуляция по-умному
До появления ES6 Modules разработчики использовали паттерн Module для создания приватных и публичных методов. Даже сейчас, когда у нас есть import/export, этот подход остается полезным для организации логики внутри одного файла.
Секрет — в замыканиях (closures). Мы создаем функцию, которая возвращает объект с методами, а внутренние переменные остаются недоступными снаружи.
Пример: Корзина интернет-магазина
const ShoppingCart = (function() {
// Приватные переменные
let items = [];
// Приватный метод
function calculateTotal() {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// Публичный API
return {
addItem(product) {
const existing = items.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
items.push({ ...product, quantity: 1 });
}
},
removeItem(productId) {
items = items.filter(item => item.id !== productId);
},
getTotal() {
return calculateTotal();
},
clear() {
items = [];
}
};
})();
// Использование
ShoppingCart.addItem({ id: 1, name: 'Книга', price: 500 });
ShoppingCart.addItem({ id: 1, name: 'Книга', price: 500 });
console.log(ShoppingCart.getTotal()); // 1000
// ShoppingCart.items — undefined (приватная!)
Зачем это нужно: вы скрываете внутреннюю реализацию и защищаете данные от случайного изменения. Это один из базовых js паттернов, который учит дисциплине.
3. Observer (Наблюдатель): всевидящее око
Представьте, что у вас есть система уведомлений. Когда пользователь отправляет форму, нужно обновить три разных компонента: счетчик, список сообщений и звуковое оповещение. Как сделать это элегантно?
Паттерн Observer позволяет одним объектам (субъектам) оповещать множество других (наблюдателей) о своих изменениях. Это основа реактивного программирования.
Пример: Событийная шина для чата
class EventBus {
constructor() {
this.listeners = {};
}
// Подписаться на событие
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
// Возвращаем функцию для отписки
return () => {
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
};
}
// Вызвать событие
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
}
// Использование
const bus = new EventBus();
// Подписчики
const unsubscribe = bus.on('newMessage', (msg) => {
console.log(`Новое сообщение: ${msg.text}`);
});
bus.on('newMessage', (msg) => {
updateBadge(msg.chatId);
});
// Где-то в коде
bus.emit('newMessage', { text: 'Привет!', chatId: 123 });
// Отписка
unsubscribe();
Когда использовать: когда у вас есть связь «один ко многим» и нужно избежать жесткой связанности. Observer javascript часто встречается в фреймворках (например, Vue.js использует его для реактивности).
4. Factory Method (Фабричный метод): создание без головной боли
Бывает, что тип создаваемого объекта зависит от входных данных. Например, вы пишете UI-библиотеку, где кнопки, поля ввода и чекбоксы — это разные классы, но создаются они через один интерфейс.
Factory Method выносит логику создания объектов в отдельный метод. Это избавляет от кучи условий if/else и делает код гибким.
Пример: Генератор элементов формы
// Разные типы полей
class TextInput {
render() { return '<input type="text" />'; }
}
class Checkbox {
render() { return '<input type="checkbox" />'; }
}
class Button {
render() { return '<button>Click</button>'; }
}
// Фабрика
class FormElementFactory {
static createElement(type) {
switch(type) {
case 'text':
return new TextInput();
case 'checkbox':