Продвинутые паттерны проектирования в JavaScript: от теории к практике
Паттерны проектирования — это проверенные решения типовых задач в разработке программного обеспечения. Для JavaScript-разработчика, работающего с Node.js, понимание продвинутых паттернов открывает путь к созданию масштабируемых, гибких и легко поддерживаемых систем. В этой статье мы разберем четыре ключевых паттерна: Observer, Proxy, Chain of Responsibility и Strategy. Каждый будет проиллюстрирован практическим примером на Node.js.
1. Паттерн Observer (Наблюдатель): событийная архитектура в Node.js
Observer — один из фундаментальных поведенческих паттернов. Он определяет зависимость "один ко многим" между объектами: когда один объект (субъект) изменяет своё состояние, все зависимые объекты (наблюдатели) автоматически уведомляются и обновляются. В Node.js этот паттерн лежит в основе модуля EventEmitter.
Пример: система уведомлений для интернет-магазина
Представим, что у нас есть сервер, который управляет заказами. При изменении статуса заказа нужно уведомить несколько подсистем: логирование, отправку email и обновление аналитики.
const EventEmitter = require('events');
// Субъект (Subject)
class OrderManager extends EventEmitter {
constructor() {
super();
this.orders = [];
}
createOrder(orderId, items) {
const order = { orderId, items, status: 'created', createdAt: new Date() };
this.orders.push(order);
this.emit('order:created', order);
return order;
}
updateStatus(orderId, newStatus) {
const order = this.orders.find(o => o.orderId === orderId);
if (order) {
order.status = newStatus;
this.emit('order:statusChanged', order);
}
}
}
// Наблюдатели (Observers)
function logObserver(order) {
console.log(`[LOG] Order ${order.orderId}: status changed to ${order.status} at ${new Date().toISOString()}`);
}
function emailObserver(order) {
console.log(`[EMAIL] Sending notification for order ${order.orderId} with status ${order.status}`);
}
function analyticsObserver(order) {
console.log(`[ANALYTICS] Recording event: order ${order.orderId} -> ${order.status}`);
}
// Использование
const manager = new OrderManager();
manager.on('order:created', logObserver);
manager.on('order:statusChanged', logObserver);
manager.on('order:statusChanged', emailObserver);
manager.on('order:statusChanged', analyticsObserver);
manager.createOrder('ORD-001', ['Book', 'Pen']);
manager.updateStatus('ORD-001', 'shipped');
Этот подход позволяет добавлять или удалять наблюдателей без изменения кода субъекта, что делает систему гибкой и легко расширяемой.
2. Паттерн Proxy (Заместитель): контроль доступа и кэширование
Proxy предоставляет объект-заменитель, который управляет доступом к другому объекту. В JavaScript этот паттерн реализуется через встроенный объект Proxy, который позволяет перехватывать и переопределять фундаментальные операции (доступ к свойствам, вызов функций и т.д.).
Пример: кэширующий прокси для API запросов
Допустим, у нас есть сервис, который делает дорогие запросы к внешнему API. Мы хотим кэшировать результаты, чтобы избежать повторных вызовов.
// Реальный сервис (Real Subject)
class ExternalApiService {
async fetchUser(id) {
console.log(`Fetching user ${id} from external API...`);
// Имитация задержки сети
await new Promise(resolve => setTimeout(resolve, 1000));
return { id, name: `User ${id}`, email: `user${id}@example.com` };
}
}
// Прокси (Proxy)
class CachingProxy {
constructor(realService) {
this.realService = realService;
this.cache = new Map();
}
async fetchUser(id) {
if (this.cache.has(id)) {
console.log(`Returning cached user ${id}`);
return this.cache.get(id);
}
const user = await this.realService.fetchUser(id);
this.cache.set(id, user);
return user;
}
}
// Использование
(async () => {
const service = new ExternalApiService();
const proxy = new CachingProxy(service);
// Первый вызов — идёт к реальному API
const user1 = await proxy.fetchUser(1);
console.log(user1);
// Второй вызов — из кэша
const user1Again = await proxy.fetchUser(1);
console.log(user1Again);
})();
Прокси также может использоваться для логирования, валидации данных, ленивой инициализации и управления правами доступа.
3. Паттерн Chain of Responsibility: гибкая обработка запросов
Chain of Responsibility (Цепочка обязанностей) позволяет передавать запрос последовательно по цепочке обработчиков. Каждый обработчик решает, может ли он обработать запрос, или передаёт его дальше. Этот паттерн идеально подходит для создания middleware в веб-фреймворках.
Пример: валидация и обработка HTTP-запроса
// Базовый класс обработчика
class Handler {
setNext(handler) {
this.nextHandler = handler;
return handler;
}
async handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return request;
}
}
// Конкретные обработчики
class AuthenticationHandler extends Handler {
async handle(request) {
if (!request.headers.authorization) {
throw new Error('Unauthorized: missing token');
}
console.log('Authentication passed');
return super.handle(request);
}
}
class ValidationHandler extends Handler {
async handle(request) {
if (!request.body || !request.body.userId) {
throw new Error('Validation failed: userId required');
}
console.log('Validation passed');
return super.handle(request);
}
}
class LoggingHandler extends Handler {
async handle(request) {
console.log(`[REQUEST] ${request.method} ${request.url}`);
return super.handle(request);
}
}
// Сборка цепочки
const authHandler = new AuthenticationHandler();
const validationHandler = new ValidationHandler();
const logHandler = new LoggingHandler();
authHandler.setNext(validationHandler).setNext(logHandler);
// Использование
(async () => {
const request = {
method: 'POST',
url: '/api/users',
headers: { authorization: 'Bearer token123' },
body: { userId: 42, name: 'Alice' }
};
try {
const result = await authHandler.handle(request);
console.log('Request processed successfully:', result);
} catch (error) {
console.error('Error:', error.message);
}
})();
Данный подход позволяет легко добавлять, удалять или переупорядочивать шаги обработки запроса без изменения логики каждого отдельного обработчика.
4. Паттерн Strategy (Стратегия): инкапсуляция алгоритмов
Strategy определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Это позволяет