Паттерны проектирования в JavaScript с примерами
Паттерны проектирования (design patterns) — это проверенные решения типовых задач в разработке программного обеспечения. Для JavaScript-разработчика знание паттернов особенно важно, так как язык обладает гибкой объектной моделью и широко используется как на клиенте, так и на сервере (Node.js). В этой статье мы разберем четыре ключевых паттерна с практическими примерами на JavaScript.
Почему стоит изучать паттерны проектирования?
Паттерны помогают:
- Упростить поддержку и масштабирование кода
- Сделать код более читаемым и предсказуемым
- Избежать "изобретения велосипеда"
- Улучшить коммуникацию в команде (общий язык)
1. Singleton (Одиночка)
Паттерн Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. В Node.js этот паттерн часто используется для подключения к базе данных, работы с конфигурацией или логирования.
class DatabaseConnection {
constructor() {
if (DatabaseConnection.instance) {
return DatabaseConnection.instance;
}
this.connection = null;
DatabaseConnection.instance = this;
}
connect() {
// Имитация подключения к БД
this.connection = "connected";
console.log("Подключение к БД установлено");
}
query(sql) {
if (!this.connection) {
throw new Error("Нет подключения к БД");
}
console.log(`Выполнение запроса: ${sql}`);
return { data: [] };
}
}
// Использование
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
db1.connect();
console.log(db1 === db2); // true - оба указывают на один экземпляр
db2.query("SELECT * FROM users");
Когда применять: управление общими ресурсами (БД, конфигурация, кэш). Осторожно: избыточное использование может привести к сильной связанности кода.
2. Factory (Фабрика)
Factory Method определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта. В JavaScript это часто реализуется через обычные функции или классы.
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
getPermissions() {
return [];
}
}
class Admin extends User {
getPermissions() {
return ["read", "write", "delete", "manage"];
}
}
class Editor extends User {
getPermissions() {
return ["read", "write"];
}
}
class Viewer extends User {
getPermissions() {
return ["read"];
}
}
// Фабрика
class UserFactory {
createUser(name, role) {
switch (role) {
case "admin":
return new Admin(name, role);
case "editor":
return new Editor(name, role);
case "viewer":
return new Viewer(name, role);
default:
throw new Error("Неизвестная роль");
}
}
}
// Использование
const factory = new UserFactory();
const admin = factory.createUser("Анна", "admin");
const viewer = factory.createUser("Иван", "viewer");
console.log(admin.getPermissions()); // ["read", "write", "delete", "manage"]
console.log(viewer.getPermissions()); // ["read"]
Преимущества: централизованное управление созданием объектов, легкость добавления новых типов, сокрытие логики инстанцирования.
3. Observer (Наблюдатель)
Паттерн Observer определяет зависимость "один ко многим" между объектами. Когда объект (субъект) меняет состояние, все зависимые объекты (наблюдатели) автоматически уведомляются. Это основа событийно-ориентированного программирования в Node.js.
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(listener);
return () => this.off(eventName, listener);
}
off(eventName, listener) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName]
.filter(l => l !== listener);
}
emit(eventName, ...args) {
if (!this.events[eventName]) return;
this.events[eventName].forEach(listener => {
listener(...args);
});
}
}
// Пример использования
const orderSystem = new EventEmitter();
// Подписчики (наблюдатели)
const unsubLog = orderSystem.on("orderCreated", (order) => {
console.log(`Лог: Заказ #${order.id} создан`);
});
const unsubEmail = orderSystem.on("orderCreated", (order) => {
console.log(`Email: Заказ #${order.id} подтвержден`);
});
// Субъект
class OrderService {
constructor(eventBus) {
this.eventBus = eventBus;
}
createOrder(items) {
const order = {
id: Date.now(),
items,
date: new Date()
};
console.log("Заказ создан");
this.eventBus.emit("orderCreated", order);
return order;
}
}
// Запуск
const service = new OrderService(orderSystem);
service.createOrder(["Книга", "Ручка"]);
// Отписка от лога
unsubLog();
Реальный пример в Node.js: встроенный модуль EventEmitter (из пакета events) используется в большинстве серверных приложений.
4. Module (Модуль)
Module pattern в JavaScript позволяет инкапсулировать приватные данные и методы, экспортируя только публичный интерфейс. В современном Node.js это реализуется через систему модулей ES (import/export) или CommonJS (require/module.exports).
// userService.js (ES module)
const users = []; // приватная переменная
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
export function addUser(name, email) {
const user = {
id: generateId(),
name,
email,
createdAt: new Date()
};
users.push(user);
return user;
}
export function getUser(id) {
return users.find(user => user.id === id);
}
export function getAllUsers() {
return [...users]; // возвращаем копию
}
export function deleteUser(id) {
const index = users.findIndex(user => user.id === id);
if (index !== -1) {
users.splice(index, 1);
return true;
}
return false;
}
// Использование в другом файле
import { addUser, getUser, getAllUsers } from './userService.js';
addUser("Мария", "maria@example.com");
addUser("Петр", "petr@example.com");
const user = getUser("abc123");
console.log(user);
console.log(getAllUsers().length); // 2
// users напрямую недоступен - инкапсуляция соблюдена
Преимущества модульного подхода:
- Изоля