Паттерны проектирования в JavaScript с примерами: от А до Я
Паттерны проектирования (design patterns) — это проверенные решения типовых задач, возникающих при разработке программного обеспечения. В JavaScript, особенно в среде Node.js, понимание этих шаблонов помогает писать более чистый, модульный и масштабируемый код. В этой статье мы разберём основные категории паттернов с наглядными примерами.
1. Порождающие паттерны (Creational Patterns)
Эти паттерны управляют процессом создания объектов, делая систему независимой от способа их инстанцирования.
1.1. Одиночка (Singleton)
Гарантирует, что класс имеет только один экземпляр, и предоставляет глобальную точку доступа к нему. В Node.js модули по умолчанию кэшируются, что упрощает реализацию.
class DatabaseConnection {
constructor() {
if (DatabaseConnection.instance) {
return DatabaseConnection.instance;
}
this.connection = null;
DatabaseConnection.instance = this;
}
connect() {
if (!this.connection) {
this.connection = { status: 'connected' };
console.log('Новое подключение к БД');
}
return this.connection;
}
}
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
console.log(db1 === db2); // true
1.2. Фабричный метод (Factory Method)
Определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта. Идеально подходит для создания разных типов логгеров.
class LoggerFactory {
createLogger(type) {
switch (type) {
case 'console':
return new ConsoleLogger();
case 'file':
return new FileLogger();
default:
throw new Error('Неизвестный тип логгера');
}
}
}
class ConsoleLogger {
log(message) { console.log(message); }
}
class FileLogger {
log(message) { console.log(`[Файл]: ${message}`); }
}
const factory = new LoggerFactory();
const logger = factory.createLogger('console');
logger.log('Тестовое сообщение');
2. Структурные паттерны (Structural Patterns)
Описывают способы объединения классов и объектов для создания более крупных структур.
2.1. Декоратор (Decorator)
Динамически добавляет объекту новые обязанности. В JavaScript декораторы часто реализуются через функции высшего порядка или прототипы.
class Coffee {
cost() { return 10; }
description() { return 'Кофе'; }
}
function withMilk(coffee) {
coffee.cost = () => coffee.cost() + 5;
coffee.description = () => coffee.description() + ', с молоком';
return coffee;
}
function withSugar(coffee) {
coffee.cost = () => coffee.cost() + 2;
coffee.description = () => coffee.description() + ', с сахаром';
return coffee;
}
let myCoffee = new Coffee();
myCoffee = withMilk(myCoffee);
myCoffee = withSugar(myCoffee);
console.log(`${myCoffee.description()}: ${myCoffee.cost()} руб.`); // Кофе, с молоком, с сахаром: 17 руб.
2.2. Адаптер (Adapter)
Преобразует интерфейс одного класса в другой интерфейс, ожидаемый клиентом. Особенно полезен при интеграции сторонних библиотек.
class OldAPI {
getUserData() {
return { name: 'Иван', age: 25, email: 'ivan@example.com' };
}
}
class NewAPI {
fetchUser() {
return { fullName: 'Иван Петров', yearsOld: 26, contact: 'ivan@example.com' };
}
}
class UserAdapter {
constructor(api) {
this.api = api;
}
getUserData() {
const data = this.api.fetchUser();
return {
name: data.fullName,
age: data.yearsOld,
email: data.contact
};
}
}
const adapter = new UserAdapter(new NewAPI());
console.log(adapter.getUserData()); // { name: 'Иван Петров', age: 26, email: 'ivan@example.com' }
3. Поведенческие паттерны (Behavioral Patterns)
Определяют алгоритмы и способы взаимодействия между объектами.
3.1. Наблюдатель (Observer)
Создаёт механизм подписки, позволяющий одним объектам следить за изменениями в других. Основа событийной модели Node.js.
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(data));
}
}
}
const emitter = new EventEmitter();
emitter.on('userLogin', (user) => {
console.log(`Пользователь ${user.name} вошёл в систему`);
});
emitter.emit('userLogin', { name: 'Анна' });
3.2. Стратегия (Strategy)
Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Отлично подходит для реализации различных способов сортировки или сжатия данных.
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}
sort(data) {
return this.strategy(data);
}
}
const bubbleSort = (arr) => {
const sorted = [...arr];
for (let i = 0; i < sorted.length; i++) {
for (let j = 0; j < sorted.length - i - 1; j++) {
if (sorted[j] > sorted[j + 1]) {
[sorted[j], sorted[j + 1]] = [sorted[j + 1], sorted[j]];
}
}
}
return sorted;
};
const quickSort = (arr) => {
if (arr.length <= 1) return arr;
const pivot = arr[0];
const left = arr.slice(1).filter(x => x < pivot);
const right = arr.slice(1).filter(x => x >= pivot);
return [...quickSort(left), pivot, ...quickSort(right)];
};
const sorter = new Sorter(bubbleSort);
console.log(sorter.sort([5, 3, 8, 1, 2])); // [1, 2, 3, 5, 8]
sorter.strategy = quickSort;
console.log(sorter.sort([5, 3, 8, 1, 2])); // [1, 2, 3, 5, 8]
4. Практические рекомендации для Node.js
- Не злоупотребляйте Singleton — он усложняет тестирование. Используйте только для действительно глобальных ресурсов (пулы соединений, конфигурация).
- Observer встроен в Node.js — модуль
eventsпредоставляет готовый EventEmitter. Используйте его для собственных событий. - Factory Method упрощает конфигурацию