Ответ
В разработке на Node.js я активно применяю следующие паттерны:
-
Middleware / Chain of Responsibility: Основа большинства веб-фреймворков (Express, Koa). Позволяет декомпозировать обработку запроса.
// Express middleware для логирования и аутентификации const logger = (req, res, next) => { console.log(`${req.method} ${req.url}`); next(); }; const auth = (req, res, next) => { const token = req.headers.authorization; if (!isValidToken(token)) return res.status(401).send('Unauthorized'); req.user = decodeToken(token); next(); }; app.use(logger); app.use('/api', auth); // Применяется только к маршрутам /api/* -
Repository / Data Mapper: Для абстракции работы с базой данных. Бизнес-логика работает с "репозиториями", а не с конкретными ORM-запросами.
// user.repository.js class UserRepository { constructor(model) { this.model = model; } async findById(id) { return this.model.findById(id); } async findByEmail(email) { return this.model.findOne({ email }); } } // В сервисе const userRepo = new UserRepository(UserModel); const user = await userRepo.findByEmail('test@example.com'); -
Dependency Injection (DI): Улучшает тестируемость. Вместо жёсткого импорта модулей, зависимости передаются в конструктор или функцию.
// Сервис, принимающий репозиторий как зависимость class UserService { constructor(userRepository, emailService) { this.userRepo = userRepository; this.emailService = emailService; } async register(userData) { const user = await this.userRepo.create(userData); await this.emailService.sendWelcomeEmail(user.email); return user; } } // В тестах легко подменить репозиторий на mock -
Factory: Для создания сложных объектов, например, разных типов подключений или валидаторов.
function createDatabaseConnection(type) { if (type === 'mongodb') return new MongoClient(uri); if (type === 'redis') return new RedisClient(); throw new Error(`Unsupported DB type: ${type}`); } const db = createDatabaseConnection(process.env.DB_TYPE); -
Observer / Event Emitter: Встроен в Node.js через класс
EventEmitter. Используется для организации слабосвязанной коммуникации между частями приложения.const EventEmitter = require('events'); class OrderService extends EventEmitter { async processOrder(order) { // ... логика обработки this.emit('order.processed', { orderId: order.id }); } } const orderService = new OrderService(); orderService.on('order.processed', (data) => { // Отправить уведомление, обновить кэш и т.д. console.log(`Order ${data.orderId} processed`); }); -
Module Pattern / Singleton (через кэш модулей CommonJS): Node.js сам реализует Singleton для модулей. При первом
require()модуль кэшируется, и последующие вызовы возвращают тот же экземпляр.
Эти паттерны помогают создавать поддерживаемые, тестируемые и масштабируемые приложения на Node.js.
Ответ 18+ 🔞
А, паттерны в Node.js? Ну, это как набор проверенных костылей, чтобы твой код не развалился на первом же продакшене, когда на него посмотрят косо. Ядрёна вошь, сколько же их напридумывали, но реально в работе — штук пять-шесть, не больше. Слушай, я тебе по полочкам разложу, как я это вижу.
1. Middleware / Цепочка ответственности. Это, бля, основа всех веб-фреймворков. Представь, что запрос — это мужик, который заходит в бар. Сначала его проверяет вышибала (логирование), потом бармен спрашивает паспорт (аутентификация), а уж потом он может заказать пиво (основной обработчик). Всё это — звенья одной цепи. Если на любом этапе его послали нахуй, до пива он не доберётся. В Express это выглядит прям как в жизни.
// Вышибала-логгер
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // Пропускаем дальше
};
// Бармен, проверяющий паспорт (токен)
const auth = (req, res, next) => {
const token = req.headers.authorization;
if (!isValidToken(token)) return res.status(401).send('Иди нахуй, незнакомец');
req.user = decodeToken(token); // Записываем, кто пришёл
next(); // Всё ок, иди пить
};
app.use(logger);
app.use('/api', auth); // Паспорт спрашиваем только в VIP-зону (/api)
2. Repository / Data Mapper. Это когда ты делаешь вид, что твоя база данных — это не куча SQL-запросов или кривых вызовов ORM, а благородный шкаф с документами. Ты не лезешь в этот шкаф руками, а просишь специального дворецкого (репозиторий): «Принеси-ка мне пользователя с такой-то почтой». А уж дворецкий сам знает, в какой ящик лазить. Удобно, потому что если завтра ты сменишь шкаф с MongoDB на PostgreSQL, то переписывать будешь только дворецкого, а не весь дом.
// Дворецкий UserRepository
class UserRepository {
constructor(model) { this.model = model; }
async findById(id) { return this.model.findById(id); }
async findByEmail(email) { return this.model.findOne({ email }); }
}
// А в бизнес-логике ты уже не паришься
const userRepo = new UserRepository(UserModel);
const user = await userRepo.findByEmail('test@example.com'); // «Дворецкий, найди!»
3. Dependency Injection (DI). Вот это, ёпта, мастхэв для любого, кто не хочет, чтобы его код был монолитом, который одним куском прилип к конкретной библиотеке. Суть проще пареной репы: ты не создаёшь зависимости внутри класса, а говоришь: «Мне для работы нужны вот эти штуки — подайте сюда». Их тебе засовывают снаружи. Главный плюс — для тестов. Хочешь протестировать сервис? Подсуни ему заглушку (mock) вместо реальной базы, и он даже не заметит подмены. Доверия к таким заглушкам, конечно, ноль ебать, но для тестов сойдёт.
// UserService — строптивый тип, требует подачки
class UserService {
constructor(userRepository, emailService) { // «Дайте мне дворецкого и почтальона!»
this.userRepo = userRepository;
this.emailService = emailService;
}
async register(userData) {
const user = await this.userRepo.create(userData); // Работает с тем, что дали
await this.emailService.sendWelcomeEmail(user.email);
return user;
}
}
// В тестах даём ему плюшевые игрушки вместо реальных сервисов — и он счастлив.
4. Factory (Фабрика). Ну, тут всё в названии. Не хочешь сам вручную собирать сложные объекты с кучей условий? Поручи это заводу. «Мне нужно подключение к базе, но я не знаю, к какой именно — посмотри в конфиге и сделай сам». Фабрика — это такой ленивый, но толковый сантехник, который сам принесёт нужные трубы и ключи.
function createDatabaseConnection(type) {
if (type === 'mongodb') return new MongoClient(uri); // Вот тебе ключ на 12
if (type === 'redis') return new RedisClient(); // А вот — разводной
throw new Error(`Бля, у нас таких труб нет: ${type}`); // Иди купи сам
}
const db = createDatabaseConnection(process.env.DB_TYPE); // Сантехник, работай!
5. Observer / Event Emitter. Это классика Node.js, встроенная прямо в ядро. Представь, что в твоём приложении есть один крикливый чувак (сервис заказов). Он не бегает сам и не говорит каждому: «Эй, заказ обработан, делай что надо!». Он просто орёт в пустоту: «ЗАКАЗ ОБРАБОТАН!». А кто хочет — тот услышит и отреагирует. Отправит уведомление, обновит кэш, запишет в лог. Все живут отдельно и не дергают друг друга за штаны. Красота.
const EventEmitter = require('events');
class OrderService extends EventEmitter {
async processOrder(order) {
// ... тут он колдунство своё делает
this.emit('order.processed', { orderId: order.id }); // КРИЧИТ на всю систему
}
}
const orderService = new OrderService();
// А это — ушлые слушатели, которые подсуетились
orderService.on('order.processed', (data) => {
console.log(`Опа, заказ ${data.orderId} готов! Бегу слать смс!`);
});
6. Module Pattern / Singleton (благодаря CommonJS). А это вообще магия Node.js, за которую ему отдельное спасибо. Когда ты делаешь require('./my-module') в первый раз, модуль выполняется и его экземпляр кладётся в кэш. Все последующие вызовы require возвращают этот же, уже готовый экземпляр. Получается этакий вынужденный синглтон, без лишних телодвижений. Удобно для глобальных конфигов, пулов подключений и прочих штук, которые должны быть в единственном экземпляре. Сам от себя охуеешь, когда поймёшь, как это просто работает.
Вот, собственно, и весь основной набор. Этих ребят хватает, чтобы не выстрелить себе в ногу и написать код, который через полгода не захочется вырвать с корнем и переписать. Волнение ебать, когда начинаешь, но потом втягиваешься.