Что такое Single Responsibility Principle (Принцип единственной ответственности)?

Ответ

Single Responsibility Principle (SRP) — первый принцип SOLID, который утверждает, что класс, модуль или функция должны иметь одну и только одну причину для изменения. В контексте Node.js это часто относится к модулям, сервисам или функциям.

Пример нарушения SRP в Node.js-модуле:

// Плохо: Модуль делает слишком много
class OrderProcessor {
  constructor(order) {
    this.order = order;
  }

  validate() { /* логика валидации */ }
  calculateTotal() { /* логика расчёта */ }
  saveToDatabase() { /* логика работы с БД */ }
  sendConfirmationEmail() { /* логика отправки email */ }
  generateInvoicePDF() { /* логика генерации PDF */ }
}

Такой класс изменится, если поменяется логика валидации, расчётов, схема БД, почтовый сервис или библиотека для PDF.

Пример c соблюдением SRP:

// Хорошо: Ответственности разделены
class OrderValidator {
  validate(order) { /* только валидация */ }
}

class OrderCalculator {
  calculateTotal(order) { /* только расчёт */ }
}

class OrderRepository {
  async save(order) { /* только сохранение в БД */ }
}

class EmailService {
  async sendConfirmation(order) { /* только отправка email */ }
}

class InvoiceService {
  async generate(order) { /* только генерация PDF */ }
}

// Композиция в основном сервисе
class OrderService {
  constructor(validator, calculator, repository, emailService, invoiceService) {
    this.validator = validator;
    this.calculator = calculator;
    this.repository = repository;
    this.emailService = emailService;
    this.invoiceService = invoiceService;
  }

  async process(order) {
    this.validator.validate(order);
    order.total = this.calculator.calculateTotal(order);
    await this.repository.save(order);
    await this.emailService.sendConfirmation(order);
    await this.invoiceService.generate(order);
    return order;
  }
}

Практическая польза в Node.js:

  • Тестируемость: Каждый сервис можно протестировать изолированно с помощью моков (например, jest).
  • Поддержка: Изменение в логике отправки email не затронет код валидации заказа.
  • Переиспользование: EmailService можно использовать в других контекстах (регистрация пользователя, сброс пароля).
  • Читаемость: Код становится предсказуемым, так как каждый модуль делает ровно одну понятную вещь.

Ответ 18+ 🔞

А, слушай, про этот ваш SRP, ёпта. Ну, принцип-то вроде и простой, а народ его так изнасиловать может, что мама не горюй. Типа, один класс — одна причина для изменений. А на деле выходит, что один класс делает всё: и в базу пишет, и письма шлёт, и счета генерит, и ещё, наверное, кофе тебе сварит, если попросить. Ебать мои старые костыли, ну и манда с ушами получается, а не класс.

Смотри, вот тебе живой пример, как делать НЕ НАДО. Представь себе модуль OrderProcessor. Чувак, который внутри творит такую дичь, что удивление пиздец.

// Плохо: Модуль делает слишком много
class OrderProcessor {
  constructor(order) {
    this.order = order;
  }

  validate() { /* логика валидации */ }
  calculateTotal() { /* логика расчёта */ }
  saveToDatabase() { /* логика работы с БД */ }
  sendConfirmationEmail() { /* логика отправки email */ }
  generateInvoicePDF() { /* логика генерации PDF */ }
}

Представил? Вот этот уродец. Он у тебя изменится по пяти разным причинам! Захотел ты поменять библиотеку для PDF — трогай этот класс. Почтовик сменил API — опять лезь сюда. Схему в базе поправил — и снова ты тут. Волнение ебать начинается, потому что одно изменение в одном месте может нечаянно сломать всё остальное. Доверия ебать ноль к такому коду.

А теперь смотри, как надо бы, по-человечески. Разделяем эту пиздопроебибну на нормальные, вменяемые куски.

// Хорошо: Ответственности разделены
class OrderValidator {
  validate(order) { /* только валидация */ }
}

class OrderCalculator {
  calculateTotal(order) { /* только расчёт */ }
}

class OrderRepository {
  async save(order) { /* только сохранение в БД */ }
}

class EmailService {
  async sendConfirmation(order) { /* только отправка email */ }
}

class InvoiceService {
  async generate(order) { /* только генерация PDF */ }
}

// Композиция в основном сервисе
class OrderService {
  constructor(validator, calculator, repository, emailService, invoiceService) {
    this.validator = validator;
    this.calculator = calculator;
    this.repository = repository;
    this.emailService = emailService;
    this.invoiceService = invoiceService;
  }

  async process(order) {
    this.validator.validate(order);
    order.total = this.calculator.calculateTotal(order);
    await this.repository.save(order);
    await this.emailService.sendConfirmation(order);
    await this.invoiceService.generate(order);
    return order;
  }
}

Видишь разницу? Каждый мелкий сервис — хитрая жопа, которая знает свой один маленький трюк и делает его хорошо. А главный OrderService просто как дирижёр, всех этих умельцев собирает и говорит: "Работайте, суки!"

И практическая-то польза охуенная:

  • Тестируемость: Каждого ушлёпка можно тестировать отдельно. Хочешь проверить EmailService? Подсуни ему мок заказа и смотри, шлёт он письма или нет. Ёперный театр, да это же мечта!
  • Поддержка: Поменял что-то в логике отправки писем? Ну и хуй с ним, меняй только EmailService. Валидация заказов, генерация PDF — они об этом даже не узнают, живут своей счастливой жизнью.
  • Переиспользование: Этот самый EmailService можно потом тырить для регистрации пользователей или сброса пароля. Один раз написал — пользуйся где хочешь. Красота.
  • Читаемость: Открываешь любой класс и сразу видно — а, этот чувак только валидирует. А этот — только считает. Никаких сюрпризов. Э бошка думай меньше надо, когда ищешь баг.

Вот и весь принцип. Не делай из класса универсальную давалку во все дыры. Пусть каждый занимается своим делом, и в проекте будет порядок, а не ебушки-воробушки.