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

«Как реализовывал принципы SOLID на практике» — вопрос из категории ООП, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В своих 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 эти принципы заложены в архитектуру из коробки, что делает код ещё более чистым и сопровождаемым.