Как реализовывал принципы SOLID на практике

Ответ

В своих Node.js проектах, особенно при построении сервисов на Express или NestJS, я сознательно применяю SOLID. Приведу пример из модуля обработки платежей в интернет-магазине.

1. Принцип единственной ответственности (SRP): Каждый класс отвечает за одну вещь. Вместо монолитного PaymentController я разделил логику.

// payment/NotificationService.js - Отправка уведомлений
class NotificationService {
  constructor(emailClient, smsClient) {
    this.emailClient = emailClient;
    this.smsClient = smsClient;
  }
  async sendPaymentSuccess(user, amount) {
    await this.emailClient.send(user.email, `Оплата ${amount} прошла успешно`);
    // ... логика SMS
  }
}

// payment/PaymentGateway.js - Взаимодействие с внешним платёжным шлюзом
class StripeGateway {
  async charge(amount, token) {
    // Вызов API Stripe
    return await stripe.charges.create({ amount, source: token });
  }
}

// payment/PaymentProcessor.js - Координация процесса оплаты
class PaymentProcessor {
  constructor(gateway, notificationService) {
    this.gateway = gateway;
    this.notificationService = notificationService;
  }
  async process(order, paymentToken) {
    const result = await this.gateway.charge(order.total, paymentToken);
    await this.notificationService.sendPaymentSuccess(order.user, order.total);
    return result;
  }
}

2. Принцип открытости/закрытости (OCP): Система открыта для расширения, но закрыта для модификации. Чтобы добавить новый платёжный метод (например, PayPal), не меняем существующий код.

// Абстракция платёжного шлюза
class PaymentGateway {
  async charge(amount, details) {
    throw new Error('Метод charge должен быть реализован');
  }
}

class StripeGateway extends PaymentGateway { /* ... */ }
class PayPalGateway extends PaymentGateway {
  async charge(amount, details) {
    // Реализация для PayPal API
  }
}
// Теперь PaymentProcessor может работать с любым шлюзом.

3. Принцип подстановки Барбары Лисков (LSP): Наследники PaymentGateway взаимозаменяемы. PayPalGateway можно подставить вместо StripeGateway, и PaymentProcessor продолжит работать корректно, так как контракт метода charge соблюдён.

4. Принцип разделения интерфейса (ISP): Вместо одного "толстого" интерфейса IPaymentService со методами charge, refund, getHistory, generateInvoice, я создаю специфичные интерфейсы.

// В TypeScript это выглядело бы так:
interface IPaymentExecutor {
  charge(amount: number, details: any): Promise<PaymentResult>;
}
interface IRefundHandler {
  refund(paymentId: string): Promise<void>;
}
// Класс может реализовывать только нужные ему интерфейсы.
class BasicGateway implements IPaymentExecutor { /* только charge */ }
class FullFeaturedGateway implements IPaymentExecutor, IRefundHandler { /* оба метода */ }

5. Принцип инверсии зависимостей (DIP): Модули верхнего уровня (бизнес-логика) не зависят от модулей нижнего уровня (детали реализации). Это достигается через Dependency Injection (DI).

// В Express с ручной инъекцией
const stripeGateway = new StripeGateway(process.env.STRIPE_KEY);
const notificationService = new NotificationService(emailClient, smsClient);
const paymentProcessor = new PaymentProcessor(stripeGateway, notificationService);

app.post('/pay', (req, res) => {
  // Контроллер зависит от абстракции PaymentProcessor
  await paymentProcessor.process(req.body.order, req.body.token);
  res.sendStatus(200);
});

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

  • Тестируемость: PaymentProcessor легко протестировать с мок-объектами gateway и notificationService.
  • Гибкость: Замена Stripe на другой шлюз требует изменения только в одном месте — конфигурации DI.
  • Поддержка: Из-за чёткого разделения ответственности, исправление бага в отправке email не затронет логику списания денег.

В фреймворке NestJS эти принципы заложены в архитектуру из коробки, что делает код ещё более чистым и сопровождаемым.

Ответ 18+ 🔞

Э, слушай, а вот реально, я в своих Node.js проектах, особенно когда на Express или NestJS что-то строю, сознательно SOLID применяю. Не потому что модно, а потому что потом самому не охуеть от своего же кода через полгода. Давай на примере модуля оплаты в магазине разберём, как это выглядит без этой вашей каши в голове.

1. Принцип единственной ответственности (SRP): Ёпта, это же элементарно — один класс, одна задача. Вместо того чтобы пихать всё в один PaymentController, который и деньги списывает, и письма шлёт, и в базу пишет, я это раскидал. Чисто чтобы не получилась манда с ушами, которая всё умеет, но ничего нормально.

// payment/NotificationService.js - Только уведомления, блять
class NotificationService {
  constructor(emailClient, smsClient) {
    this.emailClient = emailClient;
    this.smsClient = smsClient;
  }
  async sendPaymentSuccess(user, amount) {
    await this.emailClient.send(user.email, `Оплата ${amount} прошла успешно`);
    // ... ну и смс там если надо
  }
}

// payment/PaymentGateway.js - Только общение с внешним шлюзом
class StripeGateway {
  async charge(amount, token) {
    // Тыкаемся в API Stripe
    return await stripe.charges.create({ amount, source: token });
  }
}

// payment/PaymentProcessor.js - Главный координатор, босс
class PaymentProcessor {
  constructor(gateway, notificationService) {
    this.gateway = gateway;
    this.notificationService = notificationService;
  }
  async process(order, paymentToken) {
    const result = await this.gateway.charge(order.total, paymentToken);
    await this.notificationService.sendPaymentSuccess(order.user, order.total);
    return result;
  }
}

2. Принцип открытости/закрытости (OCP): Суть в том, чтобы систему можно было расширять, не ломая уже работающее. Захотел добавить PayPal? Да похуй, не трогай старый код, просто новый класс напиши.

// Абстракция, от которой пляшем
class PaymentGateway {
  async charge(amount, details) {
    throw new Error('Метод charge должен быть реализован, ёпта');
  }
}

class StripeGateway extends PaymentGateway { /* ... */ }
class PayPalGateway extends PaymentGateway {
  async charge(amount, details) {
    // Твоя логика для PayPal API
  }
}
// И наш PaymentProcessor будет работать с любым шлюзом, как с родным.

3. Принцип подстановки Барбары Лисков (LSP): Наследники PaymentGateway должны быть взаимозаменяемы. Если я подставлю PayPalGateway вместо StripeGateway, и всё накрылось медным тазом — значит, я распиздяй и принцип нарушил. Контракт метода charge должен соблюдаться железно.

4. Принцип разделения интерфейса (ISP): Вместо одного интерфейса IPaymentService с овердохуищей методов (charge, refund, getHistory, generateInvoice), я делаю несколько маленьких и точных. Класс берет только то, что ему нужно.

// В TypeScript это выглядит очевидно
interface IPaymentExecutor {
  charge(amount: number, details: any): Promise<PaymentResult>;
}
interface IRefundHandler {
  refund(paymentId: string): Promise<void>;
}
// Простой шлюз реализует только оплату
class BasicGateway implements IPaymentExecutor { /* только charge */ }
// А навороченный — всё сразу
class FullFeaturedGateway implements IPaymentExecutor, IRefundHandler { /* оба метода */ }

5. Принцип инверсии зависимостей (DIP): Высокоуровневая бизнес-логика не должна зависеть от низкоуровневых деталей вроде конкретной библиотеки для Stripe. Зависимости внедряются извне.

// В Express делаем инъекцию вручную
const stripeGateway = new StripeGateway(process.env.STRIPE_KEY);
const notificationService = new NotificationService(emailClient, smsClient);
const paymentProcessor = new PaymentProcessor(stripeGateway, notificationService);

app.post('/pay', (req, res) => {
  // Контроллеру похуй, что там внутри, он работает с процессором
  await paymentProcessor.process(req.body.order, req.body.token);
  res.sendStatus(200);
});

А в чём, собственно, профит?

  • Тестируемость: Чтобы протестировать PaymentProcessor, я подсуну ему заглушки (gateway и notificationService). Волнение ебать — ноль, всё изолированно.
  • Гибкость: Завтра скажут «переезжаем с Stripe на другую систему». Ни хуя себе, да не вопрос — меняем одну строчку в конфигурации, где создаём шлюз.
  • Поддержка: Баг в отправке email? Идешь прямиком в NotificationService. Логика списания денег останется нетронутой, доверия ебать к этому больше.

В NestJS это вообще из коробки зашито, там такая красота получается, что просто ебушки-воробушки. Код живёт долго и счастливо, а не превращается в пиздопроебибну, которую страшно открывать.