Design Patterns в JavaScript: паттерны проектирования
Singleton, Factory, Observer, Module и другие классические паттерны
Design patterns — проверенные решения типичных проблем. Gang of Four описали 23 паттерна в 1994 году, и они актуальны до сих пор. Разберем ключевые паттерны и их применение в JavaScript. Creational Patterns 1. Singleton Гарантирует существование только одного экземпляра класса. class Database { constructor() { if (Database.instance) { return Database.instance; } this.connection = this.connect(); Database.instance = this; } connect() { console.log('Database connected'); return { /* connection */ }; } query(sql) { return this.connection.execute(sql); } } const db1 = new Database(); const db2 = new Database(); console.log(db1 === db2); // true Modern подход (ES6 modules): // database.js class Database { connect() { /* ... */ } query(sql) { /* ... */ } } export default new Database(); // app.js import db from './database.js'; db.query('SELECT * FROM users'); 2. Factory Создает объекты без указания точного класса. class UserFactory { createUser(type) { switch (type) { case 'admin': return new Admin(); case 'moderator': return new Moderator(); case 'user': return new RegularUser(); default: throw new Error('Invalid user type'); } } } class Admin { constructor() { this.permissions = ['read', 'write', 'delete']; } } class Moderator { constructor() { this.permissions = ['read', 'write']; } } class RegularUser { constructor() { this.permissions = ['read']; } } const factory = new UserFactory(); const admin = factory.createUser('admin'); const user = factory.createUser('user'); 3. Builder Пошаговое создание сложных объектов. class RequestBuilder { constructor() { this.request = {}; } setMethod(method) { this.request.method = method; return this; } setURL(url) { this.request.url = url; return this; } setHeaders(headers) { this.request.headers = headers; return this; } setBody(body) { this.request.body = body; return this; } build() { return this.request; } } const request = new RequestBuilder() .setMethod('POST') .setURL('/api/users') .setHeaders({ 'Content-Type': 'application/json' }) .setBody({ name: 'John' }) .build(); Structural Patterns 4. Module Pattern Инкапсуляция приватных и публичных членов. const Calculator = (function() { // Private let result = 0; function log(operation, value) { console.log(`${operation}: ${value}`); } // Public API return { add(value) { result += value; log('Add', value); return this; }, subtract(value) { result -= value; log('Subtract', value); return this; }, getResult() { return result; }, reset() { result = 0; return this; } }; })(); Calculator.add(5).subtract(2).add(10); console.log(Calculator.getResult()); // 13 5. Proxy Контролирует доступ к объекту. const user = { name: 'John', age: 30, email: 'john@example.com' }; const userProxy = new Proxy(user, { get(target, property) { console.log(`Getting ${property}`); return target[property]; }, set(target, property, value) { console.log(`Setting ${property} to ${value}`); if (property === 'age' && typeof value !== 'number') { throw new Error('Age must be a number'); } target[property] = value; return true; } }); userProxy.age = 31; // Setting age to 31 console.log(userProxy.age); // Getting age, 31 Validation Proxy: function createValidator(schema) { return new Proxy({}, { set(target, property, value) { const validator = schema[property]; if (validator && !validator(value)) { throw new Error(`Invalid value for ${property}`); } target[property] = value; return true; } }); } const schema = { age: (value) => typeof value === 'number' && value >= 0, email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) }; const user = createValidator(schema); user.age = 25; // OK user.email = 'test@example.com'; // OK user.age = -1; // Error! 6. Decorator Добавляет функциональность объекту динамически. class Coffee { cost() { return 5; } description() { return 'Coffee'; } } class MilkDecorator { constructor(coffee) { this.coffee = coffee; } cost() { return this.coffee.cost() + 2; } description() { return this.coffee.description() + ', Milk'; } } class SugarDecorator { constructor(coffee) { this.coffee = coffee; } cost() { return this.coffee.cost() + 1; } description() { return this.coffee.description() + ', Sugar'; } } let myCoffee = new Coffee(); myCoffee = new MilkDecorator(myCoffee); myCoffee = new SugarDecorator(myCoffee); console.log(myCoffee.description()); // Coffee, Milk, Sugar console.log(myCoffee.cost()); // 8 Function Decorator: function time(fn) { return function(...args) { console.time(fn.name); const result = fn.apply(this, args); console.timeEnd(fn.name); return result; }; } function memoize(fn) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = fn.apply(this, args); cache.set(key, result); return result; }; } let fibonacci = (n) => n Behavioral Patterns 7. Observer (Pub/Sub) Уведомление подписчиков об изменениях. class EventEmitter { constructor() { this.events = {}; } on(event, listener) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(listener); } off(event, listenerToRemove) { if (!this.events[event]) return; this.events[event] = this.events[event].filter( listener => listener !== listenerToRemove ); } emit(event, ...args) { if (!this.events[event]) return; this.events[event].forEach(listener => { listener(...args); }); } once(event, listener) { const onceWrapper = (...args) => { listener(...args); this.off(event, onceWrapper); }; this.on(event, onceWrapper); } } const emitter = new EventEmitter(); emitter.on('userCreated', (user) => { console.log('Send welcome email to', user.email); }); emitter.on('userCreated', (user) => { console.log('Log analytics for', user.id); }); emitter.emit('userCreated', { id: 1, email: 'john@example.com' }); 8. Strategy Выбор алгоритма во время выполнения. class PaymentContext { constructor(strategy) { this.strategy = strategy; } setStrategy(strategy) { this.strategy = strategy; } pay(amount) { return this.strategy.pay(amount); } } class CreditCardStrategy { pay(amount) { console.log(`Paid ${amount} with Credit Card`); } } class PayPalStrategy { pay(amount) { console.log(`Paid ${amount} with PayPal`); } } class CryptoStrategy { pay(amount) { console.log(`Paid ${amount} with Crypto`); } } const payment = new PaymentContext(new CreditCardStrategy()); payment.pay(100); payment.setStrategy(new PayPalStrategy()); payment.pay(200); 9. Command Инкапсулирует запрос как объект. class Command { constructor(execute, undo) { this.execute = execute; this.undo = undo; } } class Calculator { constructor() { this.value = 0; this.history = []; } executeCommand(command) { this.history.push(command); command.execute(); } undo() { const command = this.history.pop(); if (command) { command.undo(); } } } const calculator = new Calculator(); const addCommand = new Command( () => calculator.value += 10, () => calculator.value -= 10 ); const multiplyCommand = new Command( () => calculator.value *= 2, () => calculator.value /= 2 ); calculator.executeCommand(addCommand); // value = 10 calculator.executeCommand(multiplyCommand); // value = 20 calculator.undo(); // value = 10 calculator.undo(); // value = 0 10. Chain of Responsibility Цепочка обработчиков запросов. class Handler { constructor() { this.nextHandler = null; } setNext(handler) { this.nextHandler = handler; return handler; } handle(request) { if (this.nextHandler) { return this.nextHandler.handle(request); } return null; } } class AuthHandler extends Handler { handle(request) { if (!request.isAuthenticated) { return 'Not authenticated'; } return super.handle(request); } } class ValidationHandler extends Handler { handle(request) { if (!request.isValid) { return 'Invalid request'; } return super.handle(request); } } class ProcessHandler extends Handler { handle(request) { return 'Request processed'; } } const auth = new AuthHandler(); const validation = new ValidationHandler(); const process = new ProcessHandler(); auth.setNext(validation).setNext(process); const result = auth.handle({ isAuthenticated: true, isValid: true }); console.log(result); // Request processed React Patterns Higher-Order Component: function withLoading(Component) { return function WithLoadingComponent({ isLoading, ...props }) { if (isLoading) { return Loading... ; } return ; }; } const UserList = withLoading(({ users }) => ( {users.map(u => {u.name} )} )); Render Props: function Mouse({ render }) { const [position, setPosition] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY }); window.addEventListener('mousemove', handleMove); return () => window.removeEventListener('mousemove', handleMove); }, []); return render(position); } Position: {x}, {y} } /> Compound Components: const TabContext = createContext(); function Tabs({ children }) { const [activeTab, setActiveTab] = useState(0); return ( {children} ); } function TabList({ children }) { return {children} ; } function Tab({ index, children }) { const { activeTab, setActiveTab } = useContext(TabContext); return ( setActiveTab(index)} > {children} ); } function TabPanel({ index, children }) { const { activeTab } = useContext(TabContext); return activeTab === index ? {children} : null; } Tab 1 Tab 2 Content 1 Content 2 Когда использовать паттерны Singleton — DB connections, config, loggers Factory — создание объектов разных типов Observer — event systems, state management Strategy — разные алгоритмы для одной задачи Decorator — добавление функциональности динамически Proxy — validation, caching, lazy loading Заключение: Design patterns — проверенные решения типичных проблем. Не используйте паттерны ради паттернов — применяйте когда они решают конкретную проблему. Понимание паттернов улучшает коммуникацию в команде и качество кода. Изучайте, практикуйте, но помните о KISS принципе.