Какие паттерны разработки знаешь и применял в Node.js?

Ответ

В разработке на Node.js я активно применяю следующие паттерны:

  1. 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/*
  2. 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');
  3. 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
  4. 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);
  5. 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`);
    });
  6. 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 возвращают этот же, уже готовый экземпляр. Получается этакий вынужденный синглтон, без лишних телодвижений. Удобно для глобальных конфигов, пулов подключений и прочих штук, которые должны быть в единственном экземпляре. Сам от себя охуеешь, когда поймёшь, как это просто работает.

Вот, собственно, и весь основной набор. Этих ребят хватает, чтобы не выстрелить себе в ногу и написать код, который через полгода не захочется вырвать с корнем и переписать. Волнение ебать, когда начинаешь, но потом втягиваешься.